Convert polygerrit to es6-modules

This change replace all HTML imports with es6-modules. The only exceptions are:
* gr-app.html file, which can be deleted only after updating the
  gerrit/httpd/raw/PolyGerritIndexHtml.soy file.
* dark-theme.html which is loaded via importHref. Must be updated manually
  later in a separate change.

This change was produced automatically by ./es6-modules-converter.sh script.
No manual changes were made.

Change-Id: I0c447dd8c05757741e2c940720652d01d9fb7d67
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index ebac1e1..e461d1d 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -14,1520 +14,1535 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function() {
-  'use strict';
+import '../../../scripts/bundled-polymer.js';
 
-  const RoutePattern = {
-    ROOT: '/',
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../gr-navigation/gr-navigation.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-reporting/gr-reporting.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import page from 'page/page.mjs';
+self.page = page;
+import {htmlTemplate} from './gr-router_html.js';
 
-    DASHBOARD: /^\/dashboard\/(.+)$/,
-    CUSTOM_DASHBOARD: /^\/dashboard\/?$/,
-    PROJECT_DASHBOARD: /^\/p\/(.+)\/\+\/dashboard\/(.+)/,
+const RoutePattern = {
+  ROOT: '/',
 
-    AGREEMENTS: /^\/settings\/agreements\/?/,
-    NEW_AGREEMENTS: /^\/settings\/new-agreement\/?/,
-    REGISTER: /^\/register(\/.*)?$/,
+  DASHBOARD: /^\/dashboard\/(.+)$/,
+  CUSTOM_DASHBOARD: /^\/dashboard\/?$/,
+  PROJECT_DASHBOARD: /^\/p\/(.+)\/\+\/dashboard\/(.+)/,
 
-    // Pattern for login and logout URLs intended to be passed-through. May
-    // include a return URL.
-    LOG_IN_OR_OUT: /\/log(in|out)(\/(.+))?$/,
+  AGREEMENTS: /^\/settings\/agreements\/?/,
+  NEW_AGREEMENTS: /^\/settings\/new-agreement\/?/,
+  REGISTER: /^\/register(\/.*)?$/,
 
-    // Pattern for a catchall route when no other pattern is matched.
-    DEFAULT: /.*/,
+  // Pattern for login and logout URLs intended to be passed-through. May
+  // include a return URL.
+  LOG_IN_OR_OUT: /\/log(in|out)(\/(.+))?$/,
 
-    // Matches /admin/groups/[uuid-]<group>
-    GROUP: /^\/admin\/groups\/(?:uuid-)?([^,]+)$/,
+  // Pattern for a catchall route when no other pattern is matched.
+  DEFAULT: /.*/,
 
-    // Redirects /groups/self to /settings/#Groups for GWT compatibility
-    GROUP_SELF: /^\/groups\/self/,
+  // Matches /admin/groups/[uuid-]<group>
+  GROUP: /^\/admin\/groups\/(?:uuid-)?([^,]+)$/,
 
-    // Matches /admin/groups/[uuid-]<group>,info (backwords compat with gwtui)
-    // Redirects to /admin/groups/[uuid-]<group>
-    GROUP_INFO: /^\/admin\/groups\/(?:uuid-)?(.+),info$/,
+  // Redirects /groups/self to /settings/#Groups for GWT compatibility
+  GROUP_SELF: /^\/groups\/self/,
 
-    // Matches /admin/groups/<group>,audit-log
-    GROUP_AUDIT_LOG: /^\/admin\/groups\/(?:uuid-)?(.+),audit-log$/,
+  // Matches /admin/groups/[uuid-]<group>,info (backwords compat with gwtui)
+  // Redirects to /admin/groups/[uuid-]<group>
+  GROUP_INFO: /^\/admin\/groups\/(?:uuid-)?(.+),info$/,
 
-    // Matches /admin/groups/[uuid-]<group>,members
-    GROUP_MEMBERS: /^\/admin\/groups\/(?:uuid-)?(.+),members$/,
+  // Matches /admin/groups/<group>,audit-log
+  GROUP_AUDIT_LOG: /^\/admin\/groups\/(?:uuid-)?(.+),audit-log$/,
 
-    // Matches /admin/groups[,<offset>][/].
-    GROUP_LIST_OFFSET: /^\/admin\/groups(,(\d+))?(\/)?$/,
-    GROUP_LIST_FILTER: '/admin/groups/q/filter::filter',
-    GROUP_LIST_FILTER_OFFSET: '/admin/groups/q/filter::filter,:offset',
+  // Matches /admin/groups/[uuid-]<group>,members
+  GROUP_MEMBERS: /^\/admin\/groups\/(?:uuid-)?(.+),members$/,
 
-    // Matches /admin/create-project
-    LEGACY_CREATE_PROJECT: /^\/admin\/create-project\/?$/,
+  // Matches /admin/groups[,<offset>][/].
+  GROUP_LIST_OFFSET: /^\/admin\/groups(,(\d+))?(\/)?$/,
+  GROUP_LIST_FILTER: '/admin/groups/q/filter::filter',
+  GROUP_LIST_FILTER_OFFSET: '/admin/groups/q/filter::filter,:offset',
 
-    // Matches /admin/create-project
-    LEGACY_CREATE_GROUP: /^\/admin\/create-group\/?$/,
+  // Matches /admin/create-project
+  LEGACY_CREATE_PROJECT: /^\/admin\/create-project\/?$/,
 
-    PROJECT_OLD: /^\/admin\/(projects)\/?(.+)?$/,
+  // Matches /admin/create-project
+  LEGACY_CREATE_GROUP: /^\/admin\/create-group\/?$/,
 
-    // Matches /admin/repos/<repo>
-    REPO: /^\/admin\/repos\/([^,]+)$/,
+  PROJECT_OLD: /^\/admin\/(projects)\/?(.+)?$/,
 
-    // Matches /admin/repos/<repo>,commands.
-    REPO_COMMANDS: /^\/admin\/repos\/(.+),commands$/,
+  // Matches /admin/repos/<repo>
+  REPO: /^\/admin\/repos\/([^,]+)$/,
 
-    // Matches /admin/repos/<repos>,access.
-    REPO_ACCESS: /^\/admin\/repos\/(.+),access$/,
+  // Matches /admin/repos/<repo>,commands.
+  REPO_COMMANDS: /^\/admin\/repos\/(.+),commands$/,
 
-    // Matches /admin/repos/<repos>,access.
-    REPO_DASHBOARDS: /^\/admin\/repos\/(.+),dashboards$/,
+  // Matches /admin/repos/<repos>,access.
+  REPO_ACCESS: /^\/admin\/repos\/(.+),access$/,
 
-    // Matches /admin/repos[,<offset>][/].
-    REPO_LIST_OFFSET: /^\/admin\/repos(,(\d+))?(\/)?$/,
-    REPO_LIST_FILTER: '/admin/repos/q/filter::filter',
-    REPO_LIST_FILTER_OFFSET: '/admin/repos/q/filter::filter,:offset',
+  // Matches /admin/repos/<repos>,access.
+  REPO_DASHBOARDS: /^\/admin\/repos\/(.+),dashboards$/,
 
-    // Matches /admin/repos/<repo>,branches[,<offset>].
-    BRANCH_LIST_OFFSET: /^\/admin\/repos\/(.+),branches(,(.+))?$/,
-    BRANCH_LIST_FILTER: '/admin/repos/:repo,branches/q/filter::filter',
-    BRANCH_LIST_FILTER_OFFSET:
-        '/admin/repos/:repo,branches/q/filter::filter,:offset',
+  // Matches /admin/repos[,<offset>][/].
+  REPO_LIST_OFFSET: /^\/admin\/repos(,(\d+))?(\/)?$/,
+  REPO_LIST_FILTER: '/admin/repos/q/filter::filter',
+  REPO_LIST_FILTER_OFFSET: '/admin/repos/q/filter::filter,:offset',
 
-    // Matches /admin/repos/<repo>,tags[,<offset>].
-    TAG_LIST_OFFSET: /^\/admin\/repos\/(.+),tags(,(.+))?$/,
-    TAG_LIST_FILTER: '/admin/repos/:repo,tags/q/filter::filter',
-    TAG_LIST_FILTER_OFFSET:
-        '/admin/repos/:repo,tags/q/filter::filter,:offset',
+  // Matches /admin/repos/<repo>,branches[,<offset>].
+  BRANCH_LIST_OFFSET: /^\/admin\/repos\/(.+),branches(,(.+))?$/,
+  BRANCH_LIST_FILTER: '/admin/repos/:repo,branches/q/filter::filter',
+  BRANCH_LIST_FILTER_OFFSET:
+      '/admin/repos/:repo,branches/q/filter::filter,:offset',
 
-    PLUGINS: /^\/plugins\/(.+)$/,
+  // Matches /admin/repos/<repo>,tags[,<offset>].
+  TAG_LIST_OFFSET: /^\/admin\/repos\/(.+),tags(,(.+))?$/,
+  TAG_LIST_FILTER: '/admin/repos/:repo,tags/q/filter::filter',
+  TAG_LIST_FILTER_OFFSET:
+      '/admin/repos/:repo,tags/q/filter::filter,:offset',
 
-    PLUGIN_LIST: /^\/admin\/plugins(\/)?$/,
+  PLUGINS: /^\/plugins\/(.+)$/,
 
-    // Matches /admin/plugins[,<offset>][/].
-    PLUGIN_LIST_OFFSET: /^\/admin\/plugins(,(\d+))?(\/)?$/,
-    PLUGIN_LIST_FILTER: '/admin/plugins/q/filter::filter',
-    PLUGIN_LIST_FILTER_OFFSET: '/admin/plugins/q/filter::filter,:offset',
+  PLUGIN_LIST: /^\/admin\/plugins(\/)?$/,
 
-    QUERY: /^\/q\/([^,]+)(,(\d+))?$/,
+  // Matches /admin/plugins[,<offset>][/].
+  PLUGIN_LIST_OFFSET: /^\/admin\/plugins(,(\d+))?(\/)?$/,
+  PLUGIN_LIST_FILTER: '/admin/plugins/q/filter::filter',
+  PLUGIN_LIST_FILTER_OFFSET: '/admin/plugins/q/filter::filter,:offset',
 
-    /**
-     * Support vestigial params from GWT UI.
-     *
-     * @see Issue 7673.
-     * @type {!RegExp}
-     */
-    QUERY_LEGACY_SUFFIX: /^\/q\/.+,n,z$/,
-
-    // Matches /c/<changeNum>/[<basePatchNum>..][<patchNum>][/].
-    CHANGE_LEGACY: /^\/c\/(\d+)\/?(((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/,
-    CHANGE_NUMBER_LEGACY: /^\/(\d+)\/?/,
-
-    // Matches
-    // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..][<patchNum|edit>].
-    // TODO(kaspern): Migrate completely to project based URLs, with backwards
-    // compatibility for change-only.
-    CHANGE: /^\/c\/(.+)\/\+\/(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/,
-
-    // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>],edit
-    CHANGE_EDIT: /^\/c\/(.+)\/\+\/(\d+)(\/(\d+))?,edit\/?$/,
-
-    // Matches
-    // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..]<patchNum|edit>/<path>.
-    // TODO(kaspern): Migrate completely to project based URLs, with backwards
-    // compatibility for change-only.
-    // eslint-disable-next-line max-len
-    DIFF: /^\/c\/(.+)\/\+\/(\d+)(\/((-?\d+|edit)(\.\.(\d+|edit))?(\/(.+))))\/?$/,
-
-    // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>]/<path>,edit[#lineNum]
-    DIFF_EDIT: /^\/c\/(.+)\/\+\/(\d+)\/(\d+|edit)\/(.+),edit(\#\d+)?$/,
-
-    // Matches non-project-relative
-    // /c/<changeNum>/[<basePatchNum>..]<patchNum>/<path>.
-    DIFF_LEGACY: /^\/c\/(\d+)\/((-?\d+|edit)(\.\.(\d+|edit))?)\/(.+)/,
-
-    // Matches diff routes using @\d+ to specify a file name (whether or not
-    // the project name is included).
-    // eslint-disable-next-line max-len
-    DIFF_LEGACY_LINENUM: /^\/c\/((.+)\/\+\/)?(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?\/(.+))?)@[ab]?\d+$/,
-
-    SETTINGS: /^\/settings\/?/,
-    SETTINGS_LEGACY: /^\/settings\/VE\/(\S+)/,
-
-    // Matches /c/<changeNum>/ /<URL tail>
-    // Catches improperly encoded URLs (context: Issue 7100)
-    IMPROPERLY_ENCODED_PLUS: /^\/c\/(.+)\/\ \/(.+)$/,
-
-    PLUGIN_SCREEN: /^\/x\/([\w-]+)\/([\w-]+)\/?/,
-
-    DOCUMENTATION_SEARCH_FILTER: '/Documentation/q/filter::filter',
-    DOCUMENTATION_SEARCH: /^\/Documentation\/q\/(.*)$/,
-    DOCUMENTATION: /^\/Documentation(\/)?(.+)?/,
-  };
+  QUERY: /^\/q\/([^,]+)(,(\d+))?$/,
 
   /**
-   * Pattern to recognize and parse the diff line locations as they appear in
-   * the hash of diff URLs. In this format, a number on its own indicates that
-   * line number in the revision of the diff. A number prefixed by either an 'a'
-   * or a 'b' indicates that line number of the base of the diff.
+   * Support vestigial params from GWT UI.
    *
-   * @type {RegExp}
+   * @see Issue 7673.
+   * @type {!RegExp}
    */
-  const LINE_ADDRESS_PATTERN = /^([ab]?)(\d+)$/;
+  QUERY_LEGACY_SUFFIX: /^\/q\/.+,n,z$/,
 
-  /**
-   * Pattern to recognize '+' in url-encoded strings for replacement with ' '.
-   */
-  const PLUS_PATTERN = /\+/g;
+  // Matches /c/<changeNum>/[<basePatchNum>..][<patchNum>][/].
+  CHANGE_LEGACY: /^\/c\/(\d+)\/?(((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/,
+  CHANGE_NUMBER_LEGACY: /^\/(\d+)\/?/,
 
-  /**
-   * Pattern to recognize leading '?' in window.location.search, for stripping.
-   */
-  const QUESTION_PATTERN = /^\?*/;
+  // Matches
+  // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..][<patchNum|edit>].
+  // TODO(kaspern): Migrate completely to project based URLs, with backwards
+  // compatibility for change-only.
+  CHANGE: /^\/c\/(.+)\/\+\/(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/,
 
-  /**
-   * GWT UI would use @\d+ at the end of a path to indicate linenum.
-   */
-  const LEGACY_LINENUM_PATTERN = /@([ab]?\d+)$/;
+  // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>],edit
+  CHANGE_EDIT: /^\/c\/(.+)\/\+\/(\d+)(\/(\d+))?,edit\/?$/,
 
-  const LEGACY_QUERY_SUFFIX_PATTERN = /,n,z$/;
+  // Matches
+  // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..]<patchNum|edit>/<path>.
+  // TODO(kaspern): Migrate completely to project based URLs, with backwards
+  // compatibility for change-only.
+  // eslint-disable-next-line max-len
+  DIFF: /^\/c\/(.+)\/\+\/(\d+)(\/((-?\d+|edit)(\.\.(\d+|edit))?(\/(.+))))\/?$/,
 
-  const REPO_TOKEN_PATTERN = /\$\{(project|repo)\}/g;
+  // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>]/<path>,edit[#lineNum]
+  DIFF_EDIT: /^\/c\/(.+)\/\+\/(\d+)\/(\d+|edit)\/(.+),edit(\#\d+)?$/,
 
-  // Polymer makes `app` intrinsically defined on the window by virtue of the
-  // custom element having the id "app", but it is made explicit here.
-  const app = document.querySelector('#app');
-  if (!app) {
-    console.log('No gr-app found (running tests)');
+  // Matches non-project-relative
+  // /c/<changeNum>/[<basePatchNum>..]<patchNum>/<path>.
+  DIFF_LEGACY: /^\/c\/(\d+)\/((-?\d+|edit)(\.\.(\d+|edit))?)\/(.+)/,
+
+  // Matches diff routes using @\d+ to specify a file name (whether or not
+  // the project name is included).
+  // eslint-disable-next-line max-len
+  DIFF_LEGACY_LINENUM: /^\/c\/((.+)\/\+\/)?(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?\/(.+))?)@[ab]?\d+$/,
+
+  SETTINGS: /^\/settings\/?/,
+  SETTINGS_LEGACY: /^\/settings\/VE\/(\S+)/,
+
+  // Matches /c/<changeNum>/ /<URL tail>
+  // Catches improperly encoded URLs (context: Issue 7100)
+  IMPROPERLY_ENCODED_PLUS: /^\/c\/(.+)\/\ \/(.+)$/,
+
+  PLUGIN_SCREEN: /^\/x\/([\w-]+)\/([\w-]+)\/?/,
+
+  DOCUMENTATION_SEARCH_FILTER: '/Documentation/q/filter::filter',
+  DOCUMENTATION_SEARCH: /^\/Documentation\/q\/(.*)$/,
+  DOCUMENTATION: /^\/Documentation(\/)?(.+)?/,
+};
+
+/**
+ * Pattern to recognize and parse the diff line locations as they appear in
+ * the hash of diff URLs. In this format, a number on its own indicates that
+ * line number in the revision of the diff. A number prefixed by either an 'a'
+ * or a 'b' indicates that line number of the base of the diff.
+ *
+ * @type {RegExp}
+ */
+const LINE_ADDRESS_PATTERN = /^([ab]?)(\d+)$/;
+
+/**
+ * Pattern to recognize '+' in url-encoded strings for replacement with ' '.
+ */
+const PLUS_PATTERN = /\+/g;
+
+/**
+ * Pattern to recognize leading '?' in window.location.search, for stripping.
+ */
+const QUESTION_PATTERN = /^\?*/;
+
+/**
+ * GWT UI would use @\d+ at the end of a path to indicate linenum.
+ */
+const LEGACY_LINENUM_PATTERN = /@([ab]?\d+)$/;
+
+const LEGACY_QUERY_SUFFIX_PATTERN = /,n,z$/;
+
+const REPO_TOKEN_PATTERN = /\$\{(project|repo)\}/g;
+
+// Polymer makes `app` intrinsically defined on the window by virtue of the
+// custom element having the id "app", but it is made explicit here.
+const app = document.querySelector('#app');
+if (!app) {
+  console.log('No gr-app found (running tests)');
+}
+
+// Setup listeners outside of the router component initialization.
+(function() {
+  const reporting = document.createElement('gr-reporting');
+
+  window.addEventListener('WebComponentsReady', () => {
+    reporting.timeEnd('WebComponentsReady');
+  });
+})();
+
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrRouter extends mixinBehaviors( [
+  Gerrit.BaseUrlBehavior,
+  Gerrit.FireBehavior,
+  Gerrit.PatchSetBehavior,
+  Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+    LegacyElementMixin(
+        PolymerElement))) {
+  static get template() { return htmlTemplate; }
+
+  static get is() { return 'gr-router'; }
+
+  static get properties() {
+    return {
+      _app: {
+        type: Object,
+        value: app,
+      },
+      _isRedirecting: Boolean,
+      // This variable is to differentiate between internal navigation (false)
+      // and for first navigation in app after loaded from server (true).
+      _isInitialLoad: {
+        type: Boolean,
+        value: true,
+      },
+    };
   }
 
-  // Setup listeners outside of the router component initialization.
-  (function() {
-    const reporting = document.createElement('gr-reporting');
+  start() {
+    if (!this._app) { return; }
+    this._startRouter();
+  }
 
-    window.addEventListener('WebComponentsReady', () => {
-      reporting.timeEnd('WebComponentsReady');
-    });
-  })();
+  _setParams(params) {
+    this._appElement().params = params;
+  }
+
+  _appElement() {
+    // In Polymer2 you have to reach through the shadow root of the app
+    // element. This obviously breaks encapsulation.
+    // TODO(brohlfs): Make this more elegant, e.g. by exposing app-element
+    // explicitly in app, or by delegating to it.
+    return document.getElementById('app-element') ||
+        document.getElementById('app').shadowRoot.getElementById(
+            'app-element');
+  }
+
+  _redirect(url) {
+    this._isRedirecting = true;
+    page.redirect(url);
+  }
 
   /**
-   * @appliesMixin Gerrit.BaseUrlMixin
-   * @appliesMixin Gerrit.FireMixin
-   * @appliesMixin Gerrit.PatchSetMixin
-   * @appliesMixin Gerrit.URLEncodingMixin
-   * @extends Polymer.Element
+   * @param {!Object} params
+   * @return {string}
    */
-  class GrRouter extends Polymer.mixinBehaviors( [
-    Gerrit.BaseUrlBehavior,
-    Gerrit.FireBehavior,
-    Gerrit.PatchSetBehavior,
-    Gerrit.URLEncodingBehavior,
-  ], Polymer.GestureEventListeners(
-      Polymer.LegacyElementMixin(
-          Polymer.Element))) {
-    static get is() { return 'gr-router'; }
+  _generateUrl(params) {
+    const base = this.getBaseUrl();
+    let url = '';
+    const Views = Gerrit.Nav.View;
 
-    static get properties() {
-      return {
-        _app: {
-          type: Object,
-          value: app,
-        },
-        _isRedirecting: Boolean,
-        // This variable is to differentiate between internal navigation (false)
-        // and for first navigation in app after loaded from server (true).
-        _isInitialLoad: {
-          type: Boolean,
-          value: true,
-        },
-      };
+    if (params.view === Views.SEARCH) {
+      url = this._generateSearchUrl(params);
+    } else if (params.view === Views.CHANGE) {
+      url = this._generateChangeUrl(params);
+    } else if (params.view === Views.DASHBOARD) {
+      url = this._generateDashboardUrl(params);
+    } else if (params.view === Views.DIFF || params.view === Views.EDIT) {
+      url = this._generateDiffOrEditUrl(params);
+    } else if (params.view === Views.GROUP) {
+      url = this._generateGroupUrl(params);
+    } else if (params.view === Views.REPO) {
+      url = this._generateRepoUrl(params);
+    } else if (params.view === Views.ROOT) {
+      url = '/';
+    } else if (params.view === Views.SETTINGS) {
+      url = this._generateSettingsUrl(params);
+    } else {
+      throw new Error('Can\'t generate');
     }
 
-    start() {
-      if (!this._app) { return; }
-      this._startRouter();
+    return base + url;
+  }
+
+  _generateWeblinks(params) {
+    const type = params.type;
+    switch (type) {
+      case Gerrit.Nav.WeblinkType.FILE:
+        return this._getFileWebLinks(params);
+      case Gerrit.Nav.WeblinkType.CHANGE:
+        return this._getChangeWeblinks(params);
+      case Gerrit.Nav.WeblinkType.PATCHSET:
+        return this._getPatchSetWeblink(params);
+      default:
+        console.warn(`Unsupported weblink ${type}!`);
+    }
+  }
+
+  _getPatchSetWeblink(params) {
+    const {commit, options} = params;
+    const {weblinks, config} = options || {};
+    const name = commit && commit.slice(0, 7);
+    const weblink = this._getBrowseCommitWeblink(weblinks, config);
+    if (!weblink || !weblink.url) {
+      return {name};
+    } else {
+      return {name, url: weblink.url};
+    }
+  }
+
+  _firstCodeBrowserWeblink(weblinks) {
+    // This is an ordered whitelist of web link types that provide direct
+    // links to the commit in the url property.
+    const codeBrowserLinks = ['gitiles', 'browse', 'gitweb'];
+    for (let i = 0; i < codeBrowserLinks.length; i++) {
+      const weblink =
+        weblinks.find(weblink => weblink.name === codeBrowserLinks[i]);
+      if (weblink) { return weblink; }
+    }
+    return null;
+  }
+
+  _getBrowseCommitWeblink(weblinks, config) {
+    if (!weblinks) { return null; }
+    let weblink;
+    // Use primary weblink if configured and exists.
+    if (config && config.gerrit && config.gerrit.primary_weblink_name) {
+      weblink = weblinks.find(
+          weblink => weblink.name === config.gerrit.primary_weblink_name
+      );
+    }
+    if (!weblink) {
+      weblink = this._firstCodeBrowserWeblink(weblinks);
+    }
+    if (!weblink) { return null; }
+    return weblink;
+  }
+
+  _getChangeWeblinks({repo, commit, options: {weblinks, config}}) {
+    if (!weblinks || !weblinks.length) return [];
+    const commitWeblink = this._getBrowseCommitWeblink(weblinks, config);
+    return weblinks.filter(weblink =>
+      !commitWeblink ||
+      !commitWeblink.name ||
+      weblink.name !== commitWeblink.name);
+  }
+
+  _getFileWebLinks({repo, commit, file, options: {weblinks}}) {
+    return weblinks;
+  }
+
+  /**
+   * @param {!Object} params
+   * @return {string}
+   */
+  _generateSearchUrl(params) {
+    let offsetExpr = '';
+    if (params.offset && params.offset > 0) {
+      offsetExpr = ',' + params.offset;
     }
 
-    _setParams(params) {
-      this._appElement().params = params;
+    if (params.query) {
+      return '/q/' + this.encodeURL(params.query, true) + offsetExpr;
     }
 
-    _appElement() {
-      // In Polymer2 you have to reach through the shadow root of the app
-      // element. This obviously breaks encapsulation.
-      // TODO(brohlfs): Make this more elegant, e.g. by exposing app-element
-      // explicitly in app, or by delegating to it.
-      return document.getElementById('app-element') ||
-          document.getElementById('app').shadowRoot.getElementById(
-              'app-element');
+    const operators = [];
+    if (params.owner) {
+      operators.push('owner:' + this.encodeURL(params.owner, false));
+    }
+    if (params.project) {
+      operators.push('project:' + this.encodeURL(params.project, false));
+    }
+    if (params.branch) {
+      operators.push('branch:' + this.encodeURL(params.branch, false));
+    }
+    if (params.topic) {
+      operators.push('topic:"' + this.encodeURL(params.topic, false) + '"');
+    }
+    if (params.hashtag) {
+      operators.push('hashtag:"' +
+          this.encodeURL(params.hashtag.toLowerCase(), false) + '"');
+    }
+    if (params.statuses) {
+      if (params.statuses.length === 1) {
+        operators.push(
+            'status:' + this.encodeURL(params.statuses[0], false));
+      } else if (params.statuses.length > 1) {
+        operators.push(
+            '(' +
+            params.statuses.map(s => `status:${this.encodeURL(s, false)}`)
+                .join(' OR ') +
+            ')');
+      }
     }
 
-    _redirect(url) {
-      this._isRedirecting = true;
-      page.redirect(url);
+    return '/q/' + operators.join('+') + offsetExpr;
+  }
+
+  /**
+   * @param {!Object} params
+   * @return {string}
+   */
+  _generateChangeUrl(params) {
+    let range = this._getPatchRangeExpression(params);
+    if (range.length) { range = '/' + range; }
+    let suffix = `${range}`;
+    if (params.querystring) {
+      suffix += '?' + params.querystring;
+    } else if (params.edit) {
+      suffix += ',edit';
+    }
+    if (params.messageHash) {
+      suffix += params.messageHash;
+    }
+    if (params.project) {
+      const encodedProject = this.encodeURL(params.project, true);
+      return `/c/${encodedProject}/+/${params.changeNum}${suffix}`;
+    } else {
+      return `/c/${params.changeNum}${suffix}`;
+    }
+  }
+
+  /**
+   * @param {!Object} params
+   * @return {string}
+   */
+  _generateDashboardUrl(params) {
+    const repoName = params.repo || params.project || null;
+    if (params.sections) {
+      // Custom dashboard.
+      const queryParams = this._sectionsToEncodedParams(params.sections,
+          repoName);
+      if (params.title) {
+        queryParams.push('title=' + encodeURIComponent(params.title));
+      }
+      const user = params.user ? params.user : '';
+      return `/dashboard/${user}?${queryParams.join('&')}`;
+    } else if (repoName) {
+      // Project dashboard.
+      const encodedRepo = this.encodeURL(repoName, true);
+      return `/p/${encodedRepo}/+/dashboard/${params.dashboard}`;
+    } else {
+      // User dashboard.
+      return `/dashboard/${params.user || 'self'}`;
+    }
+  }
+
+  /**
+   * @param {!Array<!{name: string, query: string}>} sections
+   * @param {string=} opt_repoName
+   * @return {!Array<string>}
+   */
+  _sectionsToEncodedParams(sections, opt_repoName) {
+    return sections.map(section => {
+      // If there is a repo name provided, make sure to substitute it into the
+      // ${repo} (or legacy ${project}) query tokens.
+      const query = opt_repoName ?
+        section.query.replace(REPO_TOKEN_PATTERN, opt_repoName) :
+        section.query;
+      return encodeURIComponent(section.name) + '=' +
+          encodeURIComponent(query);
+    });
+  }
+
+  /**
+   * @param {!Object} params
+   * @return {string}
+   */
+  _generateDiffOrEditUrl(params) {
+    let range = this._getPatchRangeExpression(params);
+    if (range.length) { range = '/' + range; }
+
+    let suffix = `${range}/${this.encodeURL(params.path, true)}`;
+
+    if (params.view === Gerrit.Nav.View.EDIT) { suffix += ',edit'; }
+
+    if (params.lineNum) {
+      suffix += '#';
+      if (params.leftSide) { suffix += 'b'; }
+      suffix += params.lineNum;
     }
 
-    /**
-     * @param {!Object} params
-     * @return {string}
-     */
-    _generateUrl(params) {
-      const base = this.getBaseUrl();
-      let url = '';
-      const Views = Gerrit.Nav.View;
+    if (params.project) {
+      const encodedProject = this.encodeURL(params.project, true);
+      return `/c/${encodedProject}/+/${params.changeNum}${suffix}`;
+    } else {
+      return `/c/${params.changeNum}${suffix}`;
+    }
+  }
 
-      if (params.view === Views.SEARCH) {
-        url = this._generateSearchUrl(params);
-      } else if (params.view === Views.CHANGE) {
-        url = this._generateChangeUrl(params);
-      } else if (params.view === Views.DASHBOARD) {
-        url = this._generateDashboardUrl(params);
-      } else if (params.view === Views.DIFF || params.view === Views.EDIT) {
-        url = this._generateDiffOrEditUrl(params);
-      } else if (params.view === Views.GROUP) {
-        url = this._generateGroupUrl(params);
-      } else if (params.view === Views.REPO) {
-        url = this._generateRepoUrl(params);
-      } else if (params.view === Views.ROOT) {
-        url = '/';
-      } else if (params.view === Views.SETTINGS) {
-        url = this._generateSettingsUrl(params);
+  /**
+   * @param {!Object} params
+   * @return {string}
+   */
+  _generateGroupUrl(params) {
+    let url = `/admin/groups/${this.encodeURL(params.groupId + '', true)}`;
+    if (params.detail === Gerrit.Nav.GroupDetailView.MEMBERS) {
+      url += ',members';
+    } else if (params.detail === Gerrit.Nav.GroupDetailView.LOG) {
+      url += ',audit-log';
+    }
+    return url;
+  }
+
+  /**
+   * @param {!Object} params
+   * @return {string}
+   */
+  _generateRepoUrl(params) {
+    let url = `/admin/repos/${this.encodeURL(params.repoName + '', true)}`;
+    if (params.detail === Gerrit.Nav.RepoDetailView.ACCESS) {
+      url += ',access';
+    } else if (params.detail === Gerrit.Nav.RepoDetailView.BRANCHES) {
+      url += ',branches';
+    } else if (params.detail === Gerrit.Nav.RepoDetailView.TAGS) {
+      url += ',tags';
+    } else if (params.detail === Gerrit.Nav.RepoDetailView.COMMANDS) {
+      url += ',commands';
+    } else if (params.detail === Gerrit.Nav.RepoDetailView.DASHBOARDS) {
+      url += ',dashboards';
+    }
+    return url;
+  }
+
+  /**
+   * @param {!Object} params
+   * @return {string}
+   */
+  _generateSettingsUrl(params) {
+    return '/settings';
+  }
+
+  /**
+   * Given an object of parameters, potentially including a `patchNum` or a
+   * `basePatchNum` or both, return a string representation of that range. If
+   * no range is indicated in the params, the empty string is returned.
+   *
+   * @param {!Object} params
+   * @return {string}
+   */
+  _getPatchRangeExpression(params) {
+    let range = '';
+    if (params.patchNum) { range = '' + params.patchNum; }
+    if (params.basePatchNum) { range = params.basePatchNum + '..' + range; }
+    return range;
+  }
+
+  /**
+   * Given a set of params without a project, gets the project from the rest
+   * API project lookup and then sets the app params.
+   *
+   * @param {?Object} params
+   */
+  _normalizeLegacyRouteParams(params) {
+    if (!params.changeNum) { return Promise.resolve(); }
+
+    return this.$.restAPI.getFromProjectLookup(params.changeNum)
+        .then(project => {
+          // Show a 404 and terminate if the lookup request failed. Attempting
+          // to redirect after failing to get the project loops infinitely.
+          if (!project) {
+            this._show404();
+            return;
+          }
+
+          params.project = project;
+          this._normalizePatchRangeParams(params);
+          this._redirect(this._generateUrl(params));
+        });
+  }
+
+  /**
+   * Normalizes the params object, and determines if the URL needs to be
+   * modified to fit the proper schema.
+   *
+   * @param {*} params
+   * @return {boolean} whether or not the URL needs to be upgraded.
+   */
+  _normalizePatchRangeParams(params) {
+    const hasBasePatchNum = params.basePatchNum !== null &&
+        params.basePatchNum !== undefined;
+    const hasPatchNum = params.patchNum !== null &&
+        params.patchNum !== undefined;
+    let needsRedirect = false;
+
+    // Diffing a patch against itself is invalid, so if the base and revision
+    // patches are equal clear the base.
+    if (hasBasePatchNum &&
+        this.patchNumEquals(params.basePatchNum, params.patchNum)) {
+      needsRedirect = true;
+      params.basePatchNum = null;
+    } else if (hasBasePatchNum && !hasPatchNum) {
+      // Regexes set basePatchNum instead of patchNum when only one is
+      // specified. Redirect is not needed in this case.
+      params.patchNum = params.basePatchNum;
+      params.basePatchNum = null;
+    }
+    return needsRedirect;
+  }
+
+  /**
+   * Redirect the user to login using the given return-URL for redirection
+   * after authentication success.
+   *
+   * @param {string} returnUrl
+   */
+  _redirectToLogin(returnUrl) {
+    const basePath = this.getBaseUrl() || '';
+    page(
+        '/login/' + encodeURIComponent(returnUrl.substring(basePath.length)));
+  }
+
+  /**
+   * Hashes parsed by page.js exclude "inner" hashes, so a URL like "/a#b#c"
+   * is parsed to have a hash of "b" rather than "b#c". Instead, this method
+   * parses hashes correctly. Will return an empty string if there is no hash.
+   *
+   * @param {!string} canonicalPath
+   * @return {!string} Everything after the first '#' ("a#b#c" -> "b#c").
+   */
+  _getHashFromCanonicalPath(canonicalPath) {
+    return canonicalPath.split('#').slice(1)
+        .join('#');
+  }
+
+  _parseLineAddress(hash) {
+    const match = hash.match(LINE_ADDRESS_PATTERN);
+    if (!match) { return null; }
+    return {
+      leftSide: !!match[1],
+      lineNum: parseInt(match[2], 10),
+    };
+  }
+
+  /**
+   * Check to see if the user is logged in and return a promise that only
+   * resolves if the user is logged in. If the user us not logged in, the
+   * promise is rejected and the page is redirected to the login flow.
+   *
+   * @param {!Object} data The parsed route data.
+   * @return {!Promise<!Object>} A promise yielding the original route data
+   *     (if it resolves).
+   */
+  _redirectIfNotLoggedIn(data) {
+    return this.$.restAPI.getLoggedIn().then(loggedIn => {
+      if (loggedIn) {
+        return Promise.resolve();
       } else {
-        throw new Error('Can\'t generate');
+        this._redirectToLogin(data.canonicalPath);
+        return Promise.reject(new Error());
       }
+    });
+  }
 
-      return base + url;
+  /**  Page.js middleware that warms the REST API's logged-in cache line. */
+  _loadUserMiddleware(ctx, next) {
+    this.$.restAPI.getLoggedIn().then(() => { next(); });
+  }
+
+  /**
+   * Map a route to a method on the router.
+   *
+   * @param {!string|!RegExp} pattern The page.js pattern for the route.
+   * @param {!string} handlerName The method name for the handler. If the
+   *     route is matched, the handler will be executed with `this` referring
+   *     to the component. Its return value will be discarded so that it does
+   *     not interfere with page.js.
+   * @param  {?boolean=} opt_authRedirect If true, then auth is checked before
+   *     executing the handler. If the user is not logged in, it will redirect
+   *     to the login flow and the handler will not be executed. The login
+   *     redirect specifies the matched URL to be used after successfull auth.
+   */
+  _mapRoute(pattern, handlerName, opt_authRedirect) {
+    if (!this[handlerName]) {
+      console.error('Attempted to map route to unknown method: ',
+          handlerName);
+      return;
+    }
+    page(pattern, this._loadUserMiddleware.bind(this), data => {
+      this.$.reporting.locationChanged(handlerName);
+      const promise = opt_authRedirect ?
+        this._redirectIfNotLoggedIn(data) : Promise.resolve();
+      promise.then(() => { this[handlerName](data); });
+    });
+  }
+
+  _startRouter() {
+    const base = this.getBaseUrl();
+    if (base) {
+      page.base(base);
     }
 
-    _generateWeblinks(params) {
-      const type = params.type;
-      switch (type) {
-        case Gerrit.Nav.WeblinkType.FILE:
-          return this._getFileWebLinks(params);
-        case Gerrit.Nav.WeblinkType.CHANGE:
-          return this._getChangeWeblinks(params);
-        case Gerrit.Nav.WeblinkType.PATCHSET:
-          return this._getPatchSetWeblink(params);
-        default:
-          console.warn(`Unsupported weblink ${type}!`);
+    Gerrit.Nav.setup(
+        url => { page.show(url); },
+        this._generateUrl.bind(this),
+        params => this._generateWeblinks(params),
+        x => x
+    );
+
+    page.exit('*', (ctx, next) => {
+      if (!this._isRedirecting) {
+        this.$.reporting.beforeLocationChanged();
       }
-    }
+      this._isRedirecting = false;
+      this._isInitialLoad = false;
+      next();
+    });
 
-    _getPatchSetWeblink(params) {
-      const {commit, options} = params;
-      const {weblinks, config} = options || {};
-      const name = commit && commit.slice(0, 7);
-      const weblink = this._getBrowseCommitWeblink(weblinks, config);
-      if (!weblink || !weblink.url) {
-        return {name};
-      } else {
-        return {name, url: weblink.url};
-      }
-    }
+    // Middleware
+    page((ctx, next) => {
+      document.body.scrollTop = 0;
 
-    _firstCodeBrowserWeblink(weblinks) {
-      // This is an ordered whitelist of web link types that provide direct
-      // links to the commit in the url property.
-      const codeBrowserLinks = ['gitiles', 'browse', 'gitweb'];
-      for (let i = 0; i < codeBrowserLinks.length; i++) {
-        const weblink =
-          weblinks.find(weblink => weblink.name === codeBrowserLinks[i]);
-        if (weblink) { return weblink; }
-      }
-      return null;
-    }
-
-    _getBrowseCommitWeblink(weblinks, config) {
-      if (!weblinks) { return null; }
-      let weblink;
-      // Use primary weblink if configured and exists.
-      if (config && config.gerrit && config.gerrit.primary_weblink_name) {
-        weblink = weblinks.find(
-            weblink => weblink.name === config.gerrit.primary_weblink_name
-        );
-      }
-      if (!weblink) {
-        weblink = this._firstCodeBrowserWeblink(weblinks);
-      }
-      if (!weblink) { return null; }
-      return weblink;
-    }
-
-    _getChangeWeblinks({repo, commit, options: {weblinks, config}}) {
-      if (!weblinks || !weblinks.length) return [];
-      const commitWeblink = this._getBrowseCommitWeblink(weblinks, config);
-      return weblinks.filter(weblink =>
-        !commitWeblink ||
-        !commitWeblink.name ||
-        weblink.name !== commitWeblink.name);
-    }
-
-    _getFileWebLinks({repo, commit, file, options: {weblinks}}) {
-      return weblinks;
-    }
-
-    /**
-     * @param {!Object} params
-     * @return {string}
-     */
-    _generateSearchUrl(params) {
-      let offsetExpr = '';
-      if (params.offset && params.offset > 0) {
-        offsetExpr = ',' + params.offset;
-      }
-
-      if (params.query) {
-        return '/q/' + this.encodeURL(params.query, true) + offsetExpr;
-      }
-
-      const operators = [];
-      if (params.owner) {
-        operators.push('owner:' + this.encodeURL(params.owner, false));
-      }
-      if (params.project) {
-        operators.push('project:' + this.encodeURL(params.project, false));
-      }
-      if (params.branch) {
-        operators.push('branch:' + this.encodeURL(params.branch, false));
-      }
-      if (params.topic) {
-        operators.push('topic:"' + this.encodeURL(params.topic, false) + '"');
-      }
-      if (params.hashtag) {
-        operators.push('hashtag:"' +
-            this.encodeURL(params.hashtag.toLowerCase(), false) + '"');
-      }
-      if (params.statuses) {
-        if (params.statuses.length === 1) {
-          operators.push(
-              'status:' + this.encodeURL(params.statuses[0], false));
-        } else if (params.statuses.length > 1) {
-          operators.push(
-              '(' +
-              params.statuses.map(s => `status:${this.encodeURL(s, false)}`)
-                  .join(' OR ') +
-              ')');
-        }
-      }
-
-      return '/q/' + operators.join('+') + offsetExpr;
-    }
-
-    /**
-     * @param {!Object} params
-     * @return {string}
-     */
-    _generateChangeUrl(params) {
-      let range = this._getPatchRangeExpression(params);
-      if (range.length) { range = '/' + range; }
-      let suffix = `${range}`;
-      if (params.querystring) {
-        suffix += '?' + params.querystring;
-      } else if (params.edit) {
-        suffix += ',edit';
-      }
-      if (params.messageHash) {
-        suffix += params.messageHash;
-      }
-      if (params.project) {
-        const encodedProject = this.encodeURL(params.project, true);
-        return `/c/${encodedProject}/+/${params.changeNum}${suffix}`;
-      } else {
-        return `/c/${params.changeNum}${suffix}`;
-      }
-    }
-
-    /**
-     * @param {!Object} params
-     * @return {string}
-     */
-    _generateDashboardUrl(params) {
-      const repoName = params.repo || params.project || null;
-      if (params.sections) {
-        // Custom dashboard.
-        const queryParams = this._sectionsToEncodedParams(params.sections,
-            repoName);
-        if (params.title) {
-          queryParams.push('title=' + encodeURIComponent(params.title));
-        }
-        const user = params.user ? params.user : '';
-        return `/dashboard/${user}?${queryParams.join('&')}`;
-      } else if (repoName) {
-        // Project dashboard.
-        const encodedRepo = this.encodeURL(repoName, true);
-        return `/p/${encodedRepo}/+/dashboard/${params.dashboard}`;
-      } else {
-        // User dashboard.
-        return `/dashboard/${params.user || 'self'}`;
-      }
-    }
-
-    /**
-     * @param {!Array<!{name: string, query: string}>} sections
-     * @param {string=} opt_repoName
-     * @return {!Array<string>}
-     */
-    _sectionsToEncodedParams(sections, opt_repoName) {
-      return sections.map(section => {
-        // If there is a repo name provided, make sure to substitute it into the
-        // ${repo} (or legacy ${project}) query tokens.
-        const query = opt_repoName ?
-          section.query.replace(REPO_TOKEN_PATTERN, opt_repoName) :
-          section.query;
-        return encodeURIComponent(section.name) + '=' +
-            encodeURIComponent(query);
-      });
-    }
-
-    /**
-     * @param {!Object} params
-     * @return {string}
-     */
-    _generateDiffOrEditUrl(params) {
-      let range = this._getPatchRangeExpression(params);
-      if (range.length) { range = '/' + range; }
-
-      let suffix = `${range}/${this.encodeURL(params.path, true)}`;
-
-      if (params.view === Gerrit.Nav.View.EDIT) { suffix += ',edit'; }
-
-      if (params.lineNum) {
-        suffix += '#';
-        if (params.leftSide) { suffix += 'b'; }
-        suffix += params.lineNum;
-      }
-
-      if (params.project) {
-        const encodedProject = this.encodeURL(params.project, true);
-        return `/c/${encodedProject}/+/${params.changeNum}${suffix}`;
-      } else {
-        return `/c/${params.changeNum}${suffix}`;
-      }
-    }
-
-    /**
-     * @param {!Object} params
-     * @return {string}
-     */
-    _generateGroupUrl(params) {
-      let url = `/admin/groups/${this.encodeURL(params.groupId + '', true)}`;
-      if (params.detail === Gerrit.Nav.GroupDetailView.MEMBERS) {
-        url += ',members';
-      } else if (params.detail === Gerrit.Nav.GroupDetailView.LOG) {
-        url += ',audit-log';
-      }
-      return url;
-    }
-
-    /**
-     * @param {!Object} params
-     * @return {string}
-     */
-    _generateRepoUrl(params) {
-      let url = `/admin/repos/${this.encodeURL(params.repoName + '', true)}`;
-      if (params.detail === Gerrit.Nav.RepoDetailView.ACCESS) {
-        url += ',access';
-      } else if (params.detail === Gerrit.Nav.RepoDetailView.BRANCHES) {
-        url += ',branches';
-      } else if (params.detail === Gerrit.Nav.RepoDetailView.TAGS) {
-        url += ',tags';
-      } else if (params.detail === Gerrit.Nav.RepoDetailView.COMMANDS) {
-        url += ',commands';
-      } else if (params.detail === Gerrit.Nav.RepoDetailView.DASHBOARDS) {
-        url += ',dashboards';
-      }
-      return url;
-    }
-
-    /**
-     * @param {!Object} params
-     * @return {string}
-     */
-    _generateSettingsUrl(params) {
-      return '/settings';
-    }
-
-    /**
-     * Given an object of parameters, potentially including a `patchNum` or a
-     * `basePatchNum` or both, return a string representation of that range. If
-     * no range is indicated in the params, the empty string is returned.
-     *
-     * @param {!Object} params
-     * @return {string}
-     */
-    _getPatchRangeExpression(params) {
-      let range = '';
-      if (params.patchNum) { range = '' + params.patchNum; }
-      if (params.basePatchNum) { range = params.basePatchNum + '..' + range; }
-      return range;
-    }
-
-    /**
-     * Given a set of params without a project, gets the project from the rest
-     * API project lookup and then sets the app params.
-     *
-     * @param {?Object} params
-     */
-    _normalizeLegacyRouteParams(params) {
-      if (!params.changeNum) { return Promise.resolve(); }
-
-      return this.$.restAPI.getFromProjectLookup(params.changeNum)
-          .then(project => {
-            // Show a 404 and terminate if the lookup request failed. Attempting
-            // to redirect after failing to get the project loops infinitely.
-            if (!project) {
-              this._show404();
-              return;
-            }
-
-            params.project = project;
-            this._normalizePatchRangeParams(params);
-            this._redirect(this._generateUrl(params));
-          });
-    }
-
-    /**
-     * Normalizes the params object, and determines if the URL needs to be
-     * modified to fit the proper schema.
-     *
-     * @param {*} params
-     * @return {boolean} whether or not the URL needs to be upgraded.
-     */
-    _normalizePatchRangeParams(params) {
-      const hasBasePatchNum = params.basePatchNum !== null &&
-          params.basePatchNum !== undefined;
-      const hasPatchNum = params.patchNum !== null &&
-          params.patchNum !== undefined;
-      let needsRedirect = false;
-
-      // Diffing a patch against itself is invalid, so if the base and revision
-      // patches are equal clear the base.
-      if (hasBasePatchNum &&
-          this.patchNumEquals(params.basePatchNum, params.patchNum)) {
-        needsRedirect = true;
-        params.basePatchNum = null;
-      } else if (hasBasePatchNum && !hasPatchNum) {
-        // Regexes set basePatchNum instead of patchNum when only one is
-        // specified. Redirect is not needed in this case.
-        params.patchNum = params.basePatchNum;
-        params.basePatchNum = null;
-      }
-      return needsRedirect;
-    }
-
-    /**
-     * Redirect the user to login using the given return-URL for redirection
-     * after authentication success.
-     *
-     * @param {string} returnUrl
-     */
-    _redirectToLogin(returnUrl) {
-      const basePath = this.getBaseUrl() || '';
-      page(
-          '/login/' + encodeURIComponent(returnUrl.substring(basePath.length)));
-    }
-
-    /**
-     * Hashes parsed by page.js exclude "inner" hashes, so a URL like "/a#b#c"
-     * is parsed to have a hash of "b" rather than "b#c". Instead, this method
-     * parses hashes correctly. Will return an empty string if there is no hash.
-     *
-     * @param {!string} canonicalPath
-     * @return {!string} Everything after the first '#' ("a#b#c" -> "b#c").
-     */
-    _getHashFromCanonicalPath(canonicalPath) {
-      return canonicalPath.split('#').slice(1)
-          .join('#');
-    }
-
-    _parseLineAddress(hash) {
-      const match = hash.match(LINE_ADDRESS_PATTERN);
-      if (!match) { return null; }
-      return {
-        leftSide: !!match[1],
-        lineNum: parseInt(match[2], 10),
-      };
-    }
-
-    /**
-     * Check to see if the user is logged in and return a promise that only
-     * resolves if the user is logged in. If the user us not logged in, the
-     * promise is rejected and the page is redirected to the login flow.
-     *
-     * @param {!Object} data The parsed route data.
-     * @return {!Promise<!Object>} A promise yielding the original route data
-     *     (if it resolves).
-     */
-    _redirectIfNotLoggedIn(data) {
-      return this.$.restAPI.getLoggedIn().then(loggedIn => {
-        if (loggedIn) {
-          return Promise.resolve();
-        } else {
-          this._redirectToLogin(data.canonicalPath);
-          return Promise.reject(new Error());
-        }
-      });
-    }
-
-    /**  Page.js middleware that warms the REST API's logged-in cache line. */
-    _loadUserMiddleware(ctx, next) {
-      this.$.restAPI.getLoggedIn().then(() => { next(); });
-    }
-
-    /**
-     * Map a route to a method on the router.
-     *
-     * @param {!string|!RegExp} pattern The page.js pattern for the route.
-     * @param {!string} handlerName The method name for the handler. If the
-     *     route is matched, the handler will be executed with `this` referring
-     *     to the component. Its return value will be discarded so that it does
-     *     not interfere with page.js.
-     * @param  {?boolean=} opt_authRedirect If true, then auth is checked before
-     *     executing the handler. If the user is not logged in, it will redirect
-     *     to the login flow and the handler will not be executed. The login
-     *     redirect specifies the matched URL to be used after successfull auth.
-     */
-    _mapRoute(pattern, handlerName, opt_authRedirect) {
-      if (!this[handlerName]) {
-        console.error('Attempted to map route to unknown method: ',
-            handlerName);
+      if (ctx.hash.match(RoutePattern.PLUGIN_SCREEN)) {
+        // Redirect all urls using hash #/x/plugin/screen to /x/plugin/screen
+        // This is needed to allow plugins to add basic #/x/ screen links to
+        // any location.
+        this._redirect(ctx.hash);
         return;
       }
-      page(pattern, this._loadUserMiddleware.bind(this), data => {
-        this.$.reporting.locationChanged(handlerName);
-        const promise = opt_authRedirect ?
-          this._redirectIfNotLoggedIn(data) : Promise.resolve();
-        promise.then(() => { this[handlerName](data); });
-      });
-    }
 
-    _startRouter() {
+      // Fire asynchronously so that the URL is changed by the time the event
+      // is processed.
+      this.async(() => {
+        this.fire('location-change', {
+          hash: window.location.hash,
+          pathname: window.location.pathname,
+        });
+      }, 1);
+      next();
+    });
+
+    this._mapRoute(RoutePattern.ROOT, '_handleRootRoute');
+
+    this._mapRoute(RoutePattern.DASHBOARD, '_handleDashboardRoute');
+
+    this._mapRoute(RoutePattern.CUSTOM_DASHBOARD,
+        '_handleCustomDashboardRoute');
+
+    this._mapRoute(RoutePattern.PROJECT_DASHBOARD,
+        '_handleProjectDashboardRoute');
+
+    this._mapRoute(RoutePattern.GROUP_INFO, '_handleGroupInfoRoute', true);
+
+    this._mapRoute(RoutePattern.GROUP_AUDIT_LOG, '_handleGroupAuditLogRoute',
+        true);
+
+    this._mapRoute(RoutePattern.GROUP_MEMBERS, '_handleGroupMembersRoute',
+        true);
+
+    this._mapRoute(RoutePattern.GROUP_LIST_OFFSET,
+        '_handleGroupListOffsetRoute', true);
+
+    this._mapRoute(RoutePattern.GROUP_LIST_FILTER_OFFSET,
+        '_handleGroupListFilterOffsetRoute', true);
+
+    this._mapRoute(RoutePattern.GROUP_LIST_FILTER,
+        '_handleGroupListFilterRoute', true);
+
+    this._mapRoute(RoutePattern.GROUP_SELF, '_handleGroupSelfRedirectRoute',
+        true);
+
+    this._mapRoute(RoutePattern.GROUP, '_handleGroupRoute', true);
+
+    this._mapRoute(RoutePattern.PROJECT_OLD,
+        '_handleProjectsOldRoute');
+
+    this._mapRoute(RoutePattern.REPO_COMMANDS,
+        '_handleRepoCommandsRoute', true);
+
+    this._mapRoute(RoutePattern.REPO_ACCESS,
+        '_handleRepoAccessRoute');
+
+    this._mapRoute(RoutePattern.REPO_DASHBOARDS,
+        '_handleRepoDashboardsRoute');
+
+    this._mapRoute(RoutePattern.BRANCH_LIST_OFFSET,
+        '_handleBranchListOffsetRoute');
+
+    this._mapRoute(RoutePattern.BRANCH_LIST_FILTER_OFFSET,
+        '_handleBranchListFilterOffsetRoute');
+
+    this._mapRoute(RoutePattern.BRANCH_LIST_FILTER,
+        '_handleBranchListFilterRoute');
+
+    this._mapRoute(RoutePattern.TAG_LIST_OFFSET,
+        '_handleTagListOffsetRoute');
+
+    this._mapRoute(RoutePattern.TAG_LIST_FILTER_OFFSET,
+        '_handleTagListFilterOffsetRoute');
+
+    this._mapRoute(RoutePattern.TAG_LIST_FILTER,
+        '_handleTagListFilterRoute');
+
+    this._mapRoute(RoutePattern.LEGACY_CREATE_GROUP,
+        '_handleCreateGroupRoute', true);
+
+    this._mapRoute(RoutePattern.LEGACY_CREATE_PROJECT,
+        '_handleCreateProjectRoute', true);
+
+    this._mapRoute(RoutePattern.REPO_LIST_OFFSET,
+        '_handleRepoListOffsetRoute');
+
+    this._mapRoute(RoutePattern.REPO_LIST_FILTER_OFFSET,
+        '_handleRepoListFilterOffsetRoute');
+
+    this._mapRoute(RoutePattern.REPO_LIST_FILTER,
+        '_handleRepoListFilterRoute');
+
+    this._mapRoute(RoutePattern.REPO, '_handleRepoRoute');
+
+    this._mapRoute(RoutePattern.PLUGINS, '_handlePassThroughRoute');
+
+    this._mapRoute(RoutePattern.PLUGIN_LIST_OFFSET,
+        '_handlePluginListOffsetRoute', true);
+
+    this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER_OFFSET,
+        '_handlePluginListFilterOffsetRoute', true);
+
+    this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER,
+        '_handlePluginListFilterRoute', true);
+
+    this._mapRoute(RoutePattern.PLUGIN_LIST, '_handlePluginListRoute', true);
+
+    this._mapRoute(RoutePattern.QUERY_LEGACY_SUFFIX,
+        '_handleQueryLegacySuffixRoute');
+
+    this._mapRoute(RoutePattern.QUERY, '_handleQueryRoute');
+
+    this._mapRoute(RoutePattern.DIFF_LEGACY_LINENUM, '_handleLegacyLinenum');
+
+    this._mapRoute(RoutePattern.CHANGE_NUMBER_LEGACY,
+        '_handleChangeNumberLegacyRoute');
+
+    this._mapRoute(RoutePattern.DIFF_EDIT, '_handleDiffEditRoute', true);
+
+    this._mapRoute(RoutePattern.CHANGE_EDIT, '_handleChangeEditRoute', true);
+
+    this._mapRoute(RoutePattern.DIFF, '_handleDiffRoute');
+
+    this._mapRoute(RoutePattern.CHANGE, '_handleChangeRoute');
+
+    this._mapRoute(RoutePattern.CHANGE_LEGACY, '_handleChangeLegacyRoute');
+
+    this._mapRoute(RoutePattern.DIFF_LEGACY, '_handleDiffLegacyRoute');
+
+    this._mapRoute(RoutePattern.AGREEMENTS, '_handleAgreementsRoute', true);
+
+    this._mapRoute(RoutePattern.NEW_AGREEMENTS, '_handleNewAgreementsRoute',
+        true);
+
+    this._mapRoute(RoutePattern.SETTINGS_LEGACY,
+        '_handleSettingsLegacyRoute', true);
+
+    this._mapRoute(RoutePattern.SETTINGS, '_handleSettingsRoute', true);
+
+    this._mapRoute(RoutePattern.REGISTER, '_handleRegisterRoute');
+
+    this._mapRoute(RoutePattern.LOG_IN_OR_OUT, '_handlePassThroughRoute');
+
+    this._mapRoute(RoutePattern.IMPROPERLY_ENCODED_PLUS,
+        '_handleImproperlyEncodedPlusRoute');
+
+    this._mapRoute(RoutePattern.PLUGIN_SCREEN, '_handlePluginScreen');
+
+    this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH_FILTER,
+        '_handleDocumentationSearchRoute');
+
+    // redirects /Documentation/q/* to /Documentation/q/filter:*
+    this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH,
+        '_handleDocumentationSearchRedirectRoute');
+
+    // Makes sure /Documentation/* links work (doin't return 404)
+    this._mapRoute(RoutePattern.DOCUMENTATION,
+        '_handleDocumentationRedirectRoute');
+
+    // Note: this route should appear last so it only catches URLs unmatched
+    // by other patterns.
+    this._mapRoute(RoutePattern.DEFAULT, '_handleDefaultRoute');
+
+    page.start();
+  }
+
+  /**
+   * @param {!Object} data
+   * @return {Promise|null} if handling the route involves asynchrony, then a
+   *     promise is returned. Otherwise, synchronous handling returns null.
+   */
+  _handleRootRoute(data) {
+    if (data.querystring.match(/^closeAfterLogin/)) {
+      // Close child window on redirect after login.
+      window.close();
+      return null;
+    }
+    let hash = this._getHashFromCanonicalPath(data.canonicalPath);
+    // For backward compatibility with GWT links.
+    if (hash) {
+      // In certain login flows the server may redirect to a hash without
+      // a leading slash, which page.js doesn't handle correctly.
+      if (hash[0] !== '/') {
+        hash = '/' + hash;
+      }
+      if (hash.includes('/ /') && data.canonicalPath.includes('/+/')) {
+        // Path decodes all '+' to ' ' -- this breaks project-based URLs.
+        // See Issue 6888.
+        hash = hash.replace('/ /', '/+/');
+      }
       const base = this.getBaseUrl();
-      if (base) {
-        page.base(base);
+      let newUrl = base + hash;
+      if (hash.startsWith('/VE/')) {
+        newUrl = base + '/settings' + hash;
       }
-
-      Gerrit.Nav.setup(
-          url => { page.show(url); },
-          this._generateUrl.bind(this),
-          params => this._generateWeblinks(params),
-          x => x
-      );
-
-      page.exit('*', (ctx, next) => {
-        if (!this._isRedirecting) {
-          this.$.reporting.beforeLocationChanged();
-        }
-        this._isRedirecting = false;
-        this._isInitialLoad = false;
-        next();
-      });
-
-      // Middleware
-      page((ctx, next) => {
-        document.body.scrollTop = 0;
-
-        if (ctx.hash.match(RoutePattern.PLUGIN_SCREEN)) {
-          // Redirect all urls using hash #/x/plugin/screen to /x/plugin/screen
-          // This is needed to allow plugins to add basic #/x/ screen links to
-          // any location.
-          this._redirect(ctx.hash);
-          return;
-        }
-
-        // Fire asynchronously so that the URL is changed by the time the event
-        // is processed.
-        this.async(() => {
-          this.fire('location-change', {
-            hash: window.location.hash,
-            pathname: window.location.pathname,
-          });
-        }, 1);
-        next();
-      });
-
-      this._mapRoute(RoutePattern.ROOT, '_handleRootRoute');
-
-      this._mapRoute(RoutePattern.DASHBOARD, '_handleDashboardRoute');
-
-      this._mapRoute(RoutePattern.CUSTOM_DASHBOARD,
-          '_handleCustomDashboardRoute');
-
-      this._mapRoute(RoutePattern.PROJECT_DASHBOARD,
-          '_handleProjectDashboardRoute');
-
-      this._mapRoute(RoutePattern.GROUP_INFO, '_handleGroupInfoRoute', true);
-
-      this._mapRoute(RoutePattern.GROUP_AUDIT_LOG, '_handleGroupAuditLogRoute',
-          true);
-
-      this._mapRoute(RoutePattern.GROUP_MEMBERS, '_handleGroupMembersRoute',
-          true);
-
-      this._mapRoute(RoutePattern.GROUP_LIST_OFFSET,
-          '_handleGroupListOffsetRoute', true);
-
-      this._mapRoute(RoutePattern.GROUP_LIST_FILTER_OFFSET,
-          '_handleGroupListFilterOffsetRoute', true);
-
-      this._mapRoute(RoutePattern.GROUP_LIST_FILTER,
-          '_handleGroupListFilterRoute', true);
-
-      this._mapRoute(RoutePattern.GROUP_SELF, '_handleGroupSelfRedirectRoute',
-          true);
-
-      this._mapRoute(RoutePattern.GROUP, '_handleGroupRoute', true);
-
-      this._mapRoute(RoutePattern.PROJECT_OLD,
-          '_handleProjectsOldRoute');
-
-      this._mapRoute(RoutePattern.REPO_COMMANDS,
-          '_handleRepoCommandsRoute', true);
-
-      this._mapRoute(RoutePattern.REPO_ACCESS,
-          '_handleRepoAccessRoute');
-
-      this._mapRoute(RoutePattern.REPO_DASHBOARDS,
-          '_handleRepoDashboardsRoute');
-
-      this._mapRoute(RoutePattern.BRANCH_LIST_OFFSET,
-          '_handleBranchListOffsetRoute');
-
-      this._mapRoute(RoutePattern.BRANCH_LIST_FILTER_OFFSET,
-          '_handleBranchListFilterOffsetRoute');
-
-      this._mapRoute(RoutePattern.BRANCH_LIST_FILTER,
-          '_handleBranchListFilterRoute');
-
-      this._mapRoute(RoutePattern.TAG_LIST_OFFSET,
-          '_handleTagListOffsetRoute');
-
-      this._mapRoute(RoutePattern.TAG_LIST_FILTER_OFFSET,
-          '_handleTagListFilterOffsetRoute');
-
-      this._mapRoute(RoutePattern.TAG_LIST_FILTER,
-          '_handleTagListFilterRoute');
-
-      this._mapRoute(RoutePattern.LEGACY_CREATE_GROUP,
-          '_handleCreateGroupRoute', true);
-
-      this._mapRoute(RoutePattern.LEGACY_CREATE_PROJECT,
-          '_handleCreateProjectRoute', true);
-
-      this._mapRoute(RoutePattern.REPO_LIST_OFFSET,
-          '_handleRepoListOffsetRoute');
-
-      this._mapRoute(RoutePattern.REPO_LIST_FILTER_OFFSET,
-          '_handleRepoListFilterOffsetRoute');
-
-      this._mapRoute(RoutePattern.REPO_LIST_FILTER,
-          '_handleRepoListFilterRoute');
-
-      this._mapRoute(RoutePattern.REPO, '_handleRepoRoute');
-
-      this._mapRoute(RoutePattern.PLUGINS, '_handlePassThroughRoute');
-
-      this._mapRoute(RoutePattern.PLUGIN_LIST_OFFSET,
-          '_handlePluginListOffsetRoute', true);
-
-      this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER_OFFSET,
-          '_handlePluginListFilterOffsetRoute', true);
-
-      this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER,
-          '_handlePluginListFilterRoute', true);
-
-      this._mapRoute(RoutePattern.PLUGIN_LIST, '_handlePluginListRoute', true);
-
-      this._mapRoute(RoutePattern.QUERY_LEGACY_SUFFIX,
-          '_handleQueryLegacySuffixRoute');
-
-      this._mapRoute(RoutePattern.QUERY, '_handleQueryRoute');
-
-      this._mapRoute(RoutePattern.DIFF_LEGACY_LINENUM, '_handleLegacyLinenum');
-
-      this._mapRoute(RoutePattern.CHANGE_NUMBER_LEGACY,
-          '_handleChangeNumberLegacyRoute');
-
-      this._mapRoute(RoutePattern.DIFF_EDIT, '_handleDiffEditRoute', true);
-
-      this._mapRoute(RoutePattern.CHANGE_EDIT, '_handleChangeEditRoute', true);
-
-      this._mapRoute(RoutePattern.DIFF, '_handleDiffRoute');
-
-      this._mapRoute(RoutePattern.CHANGE, '_handleChangeRoute');
-
-      this._mapRoute(RoutePattern.CHANGE_LEGACY, '_handleChangeLegacyRoute');
-
-      this._mapRoute(RoutePattern.DIFF_LEGACY, '_handleDiffLegacyRoute');
-
-      this._mapRoute(RoutePattern.AGREEMENTS, '_handleAgreementsRoute', true);
-
-      this._mapRoute(RoutePattern.NEW_AGREEMENTS, '_handleNewAgreementsRoute',
-          true);
-
-      this._mapRoute(RoutePattern.SETTINGS_LEGACY,
-          '_handleSettingsLegacyRoute', true);
-
-      this._mapRoute(RoutePattern.SETTINGS, '_handleSettingsRoute', true);
-
-      this._mapRoute(RoutePattern.REGISTER, '_handleRegisterRoute');
-
-      this._mapRoute(RoutePattern.LOG_IN_OR_OUT, '_handlePassThroughRoute');
-
-      this._mapRoute(RoutePattern.IMPROPERLY_ENCODED_PLUS,
-          '_handleImproperlyEncodedPlusRoute');
-
-      this._mapRoute(RoutePattern.PLUGIN_SCREEN, '_handlePluginScreen');
-
-      this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH_FILTER,
-          '_handleDocumentationSearchRoute');
-
-      // redirects /Documentation/q/* to /Documentation/q/filter:*
-      this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH,
-          '_handleDocumentationSearchRedirectRoute');
-
-      // Makes sure /Documentation/* links work (doin't return 404)
-      this._mapRoute(RoutePattern.DOCUMENTATION,
-          '_handleDocumentationRedirectRoute');
-
-      // Note: this route should appear last so it only catches URLs unmatched
-      // by other patterns.
-      this._mapRoute(RoutePattern.DEFAULT, '_handleDefaultRoute');
-
-      page.start();
+      this._redirect(newUrl);
+      return null;
     }
+    return this.$.restAPI.getLoggedIn().then(loggedIn => {
+      if (loggedIn) {
+        this._redirect('/dashboard/self');
+      } else {
+        this._redirect('/q/status:open');
+      }
+    });
+  }
 
-    /**
-     * @param {!Object} data
-     * @return {Promise|null} if handling the route involves asynchrony, then a
-     *     promise is returned. Otherwise, synchronous handling returns null.
-     */
-    _handleRootRoute(data) {
-      if (data.querystring.match(/^closeAfterLogin/)) {
-        // Close child window on redirect after login.
-        window.close();
-        return null;
+  /**
+   * Decode an application/x-www-form-urlencoded string.
+   *
+   * @param {string} qs The application/x-www-form-urlencoded string.
+   * @return {string} The decoded string.
+   */
+  _decodeQueryString(qs) {
+    return decodeURIComponent(qs.replace(PLUS_PATTERN, ' '));
+  }
+
+  /**
+   * Parse a query string (e.g. window.location.search) into an array of
+   * name/value pairs.
+   *
+   * @param {string} qs The application/x-www-form-urlencoded query string.
+   * @return {!Array<!Array<string>>} An array of name/value pairs, where each
+   *     element is a 2-element array.
+   */
+  _parseQueryString(qs) {
+    qs = qs.replace(QUESTION_PATTERN, '');
+    if (!qs) {
+      return [];
+    }
+    const params = [];
+    qs.split('&').forEach(param => {
+      const idx = param.indexOf('=');
+      let name;
+      let value;
+      if (idx < 0) {
+        name = this._decodeQueryString(param);
+        value = '';
+      } else {
+        name = this._decodeQueryString(param.substring(0, idx));
+        value = this._decodeQueryString(param.substring(idx + 1));
       }
-      let hash = this._getHashFromCanonicalPath(data.canonicalPath);
-      // For backward compatibility with GWT links.
-      if (hash) {
-        // In certain login flows the server may redirect to a hash without
-        // a leading slash, which page.js doesn't handle correctly.
-        if (hash[0] !== '/') {
-          hash = '/' + hash;
-        }
-        if (hash.includes('/ /') && data.canonicalPath.includes('/+/')) {
-          // Path decodes all '+' to ' ' -- this breaks project-based URLs.
-          // See Issue 6888.
-          hash = hash.replace('/ /', '/+/');
-        }
-        const base = this.getBaseUrl();
-        let newUrl = base + hash;
-        if (hash.startsWith('/VE/')) {
-          newUrl = base + '/settings' + hash;
-        }
-        this._redirect(newUrl);
-        return null;
+      if (name) {
+        params.push([name, value]);
       }
-      return this.$.restAPI.getLoggedIn().then(loggedIn => {
-        if (loggedIn) {
-          this._redirect('/dashboard/self');
+    });
+    return params;
+  }
+
+  /**
+   * Handle dashboard routes. These may be user, or project dashboards.
+   *
+   * @param {!Object} data The parsed route data.
+   */
+  _handleDashboardRoute(data) {
+    // User dashboard. We require viewing user to be logged in, else we
+    // redirect to login for self dashboard or simple owner search for
+    // other user dashboard.
+    return this.$.restAPI.getLoggedIn().then(loggedIn => {
+      if (!loggedIn) {
+        if (data.params[0].toLowerCase() === 'self') {
+          this._redirectToLogin(data.canonicalPath);
         } else {
-          this._redirect('/q/status:open');
+          this._redirect('/q/owner:' + encodeURIComponent(data.params[0]));
         }
-      });
-    }
-
-    /**
-     * Decode an application/x-www-form-urlencoded string.
-     *
-     * @param {string} qs The application/x-www-form-urlencoded string.
-     * @return {string} The decoded string.
-     */
-    _decodeQueryString(qs) {
-      return decodeURIComponent(qs.replace(PLUS_PATTERN, ' '));
-    }
-
-    /**
-     * Parse a query string (e.g. window.location.search) into an array of
-     * name/value pairs.
-     *
-     * @param {string} qs The application/x-www-form-urlencoded query string.
-     * @return {!Array<!Array<string>>} An array of name/value pairs, where each
-     *     element is a 2-element array.
-     */
-    _parseQueryString(qs) {
-      qs = qs.replace(QUESTION_PATTERN, '');
-      if (!qs) {
-        return [];
-      }
-      const params = [];
-      qs.split('&').forEach(param => {
-        const idx = param.indexOf('=');
-        let name;
-        let value;
-        if (idx < 0) {
-          name = this._decodeQueryString(param);
-          value = '';
-        } else {
-          name = this._decodeQueryString(param.substring(0, idx));
-          value = this._decodeQueryString(param.substring(idx + 1));
-        }
-        if (name) {
-          params.push([name, value]);
-        }
-      });
-      return params;
-    }
-
-    /**
-     * Handle dashboard routes. These may be user, or project dashboards.
-     *
-     * @param {!Object} data The parsed route data.
-     */
-    _handleDashboardRoute(data) {
-      // User dashboard. We require viewing user to be logged in, else we
-      // redirect to login for self dashboard or simple owner search for
-      // other user dashboard.
-      return this.$.restAPI.getLoggedIn().then(loggedIn => {
-        if (!loggedIn) {
-          if (data.params[0].toLowerCase() === 'self') {
-            this._redirectToLogin(data.canonicalPath);
-          } else {
-            this._redirect('/q/owner:' + encodeURIComponent(data.params[0]));
-          }
-        } else {
-          this._setParams({
-            view: Gerrit.Nav.View.DASHBOARD,
-            user: data.params[0],
-          });
-        }
-      });
-    }
-
-    /**
-     * Handle custom dashboard routes.
-     *
-     * @param {!Object} data The parsed route data.
-     * @param {string=} opt_qs Optional query string associated with the route.
-     *     If not given, window.location.search is used. (Used by tests).
-     */
-    _handleCustomDashboardRoute(data, opt_qs) {
-      // opt_qs may be provided by a test, and it may have a falsy value
-      const qs = opt_qs !== undefined ? opt_qs : window.location.search;
-      const queryParams = this._parseQueryString(qs);
-      let title = 'Custom Dashboard';
-      const titleParam = queryParams.find(
-          elem => elem[0].toLowerCase() === 'title');
-      if (titleParam) {
-        title = titleParam[1];
-      }
-      // Dashboards support a foreach param which adds a base query to any
-      // additional query.
-      const forEachParam = queryParams.find(
-          elem => elem[0].toLowerCase() === 'foreach');
-      let forEachQuery = null;
-      if (forEachParam) {
-        forEachQuery = forEachParam[1];
-      }
-      const sectionParams = queryParams.filter(
-          elem => elem[0] && elem[1] && elem[0].toLowerCase() !== 'title' &&
-          elem[0].toLowerCase() !== 'foreach');
-      const sections = sectionParams.map(elem => {
-        const query = forEachQuery ? `${forEachQuery} ${elem[1]}` : elem[1];
-        return {
-          name: elem[0],
-          query,
-        };
-      });
-
-      if (sections.length > 0) {
-        // Custom dashboard view.
+      } else {
         this._setParams({
           view: Gerrit.Nav.View.DASHBOARD,
-          user: 'self',
-          sections,
-          title,
+          user: data.params[0],
         });
-        return Promise.resolve();
       }
+    });
+  }
 
-      // Redirect /dashboard/ -> /dashboard/self.
-      this._redirect('/dashboard/self');
+  /**
+   * Handle custom dashboard routes.
+   *
+   * @param {!Object} data The parsed route data.
+   * @param {string=} opt_qs Optional query string associated with the route.
+   *     If not given, window.location.search is used. (Used by tests).
+   */
+  _handleCustomDashboardRoute(data, opt_qs) {
+    // opt_qs may be provided by a test, and it may have a falsy value
+    const qs = opt_qs !== undefined ? opt_qs : window.location.search;
+    const queryParams = this._parseQueryString(qs);
+    let title = 'Custom Dashboard';
+    const titleParam = queryParams.find(
+        elem => elem[0].toLowerCase() === 'title');
+    if (titleParam) {
+      title = titleParam[1];
+    }
+    // Dashboards support a foreach param which adds a base query to any
+    // additional query.
+    const forEachParam = queryParams.find(
+        elem => elem[0].toLowerCase() === 'foreach');
+    let forEachQuery = null;
+    if (forEachParam) {
+      forEachQuery = forEachParam[1];
+    }
+    const sectionParams = queryParams.filter(
+        elem => elem[0] && elem[1] && elem[0].toLowerCase() !== 'title' &&
+        elem[0].toLowerCase() !== 'foreach');
+    const sections = sectionParams.map(elem => {
+      const query = forEachQuery ? `${forEachQuery} ${elem[1]}` : elem[1];
+      return {
+        name: elem[0],
+        query,
+      };
+    });
+
+    if (sections.length > 0) {
+      // Custom dashboard view.
+      this._setParams({
+        view: Gerrit.Nav.View.DASHBOARD,
+        user: 'self',
+        sections,
+        title,
+      });
       return Promise.resolve();
     }
 
-    _handleProjectDashboardRoute(data) {
-      const project = data.params[0];
-      this._setParams({
-        view: Gerrit.Nav.View.DASHBOARD,
-        project,
-        dashboard: decodeURIComponent(data.params[1]),
-      });
-      this.$.reporting.setRepoName(project);
-    }
+    // Redirect /dashboard/ -> /dashboard/self.
+    this._redirect('/dashboard/self');
+    return Promise.resolve();
+  }
 
-    _handleGroupInfoRoute(data) {
-      this._redirect('/admin/groups/' + encodeURIComponent(data.params[0]));
-    }
+  _handleProjectDashboardRoute(data) {
+    const project = data.params[0];
+    this._setParams({
+      view: Gerrit.Nav.View.DASHBOARD,
+      project,
+      dashboard: decodeURIComponent(data.params[1]),
+    });
+    this.$.reporting.setRepoName(project);
+  }
 
-    _handleGroupSelfRedirectRoute(data) {
-      this._redirect('/settings/#Groups');
-    }
+  _handleGroupInfoRoute(data) {
+    this._redirect('/admin/groups/' + encodeURIComponent(data.params[0]));
+  }
 
-    _handleGroupRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.GROUP,
-        groupId: data.params[0],
-      });
-    }
+  _handleGroupSelfRedirectRoute(data) {
+    this._redirect('/settings/#Groups');
+  }
 
-    _handleGroupAuditLogRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.GROUP,
-        detail: Gerrit.Nav.GroupDetailView.LOG,
-        groupId: data.params[0],
-      });
-    }
+  _handleGroupRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.GROUP,
+      groupId: data.params[0],
+    });
+  }
 
-    _handleGroupMembersRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.GROUP,
-        detail: Gerrit.Nav.GroupDetailView.MEMBERS,
-        groupId: data.params[0],
-      });
-    }
+  _handleGroupAuditLogRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.GROUP,
+      detail: Gerrit.Nav.GroupDetailView.LOG,
+      groupId: data.params[0],
+    });
+  }
 
-    _handleGroupListOffsetRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-admin-group-list',
-        offset: data.params[1] || 0,
-        filter: null,
-        openCreateModal: data.hash === 'create',
-      });
-    }
+  _handleGroupMembersRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.GROUP,
+      detail: Gerrit.Nav.GroupDetailView.MEMBERS,
+      groupId: data.params[0],
+    });
+  }
 
-    _handleGroupListFilterOffsetRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-admin-group-list',
-        offset: data.params.offset,
-        filter: data.params.filter,
-      });
-    }
+  _handleGroupListOffsetRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.ADMIN,
+      adminView: 'gr-admin-group-list',
+      offset: data.params[1] || 0,
+      filter: null,
+      openCreateModal: data.hash === 'create',
+    });
+  }
 
-    _handleGroupListFilterRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-admin-group-list',
-        filter: data.params.filter || null,
-      });
-    }
+  _handleGroupListFilterOffsetRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.ADMIN,
+      adminView: 'gr-admin-group-list',
+      offset: data.params.offset,
+      filter: data.params.filter,
+    });
+  }
 
-    _handleProjectsOldRoute(data) {
-      let params = '';
-      if (data.params[1]) {
-        params = encodeURIComponent(data.params[1]);
-        if (data.params[1].includes(',')) {
-          params =
-              encodeURIComponent(data.params[1]).replace('%2C', ',');
-        }
-      }
+  _handleGroupListFilterRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.ADMIN,
+      adminView: 'gr-admin-group-list',
+      filter: data.params.filter || null,
+    });
+  }
 
-      this._redirect(`/admin/repos/${params}`);
-    }
-
-    _handleRepoCommandsRoute(data) {
-      const repo = data.params[0];
-      this._setParams({
-        view: Gerrit.Nav.View.REPO,
-        detail: Gerrit.Nav.RepoDetailView.COMMANDS,
-        repo,
-      });
-      this.$.reporting.setRepoName(repo);
-    }
-
-    _handleRepoAccessRoute(data) {
-      const repo = data.params[0];
-      this._setParams({
-        view: Gerrit.Nav.View.REPO,
-        detail: Gerrit.Nav.RepoDetailView.ACCESS,
-        repo,
-      });
-      this.$.reporting.setRepoName(repo);
-    }
-
-    _handleRepoDashboardsRoute(data) {
-      const repo = data.params[0];
-      this._setParams({
-        view: Gerrit.Nav.View.REPO,
-        detail: Gerrit.Nav.RepoDetailView.DASHBOARDS,
-        repo,
-      });
-      this.$.reporting.setRepoName(repo);
-    }
-
-    _handleBranchListOffsetRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.REPO,
-        detail: Gerrit.Nav.RepoDetailView.BRANCHES,
-        repo: data.params[0],
-        offset: data.params[2] || 0,
-        filter: null,
-      });
-    }
-
-    _handleBranchListFilterOffsetRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.REPO,
-        detail: Gerrit.Nav.RepoDetailView.BRANCHES,
-        repo: data.params.repo,
-        offset: data.params.offset,
-        filter: data.params.filter,
-      });
-    }
-
-    _handleBranchListFilterRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.REPO,
-        detail: Gerrit.Nav.RepoDetailView.BRANCHES,
-        repo: data.params.repo,
-        filter: data.params.filter || null,
-      });
-    }
-
-    _handleTagListOffsetRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.REPO,
-        detail: Gerrit.Nav.RepoDetailView.TAGS,
-        repo: data.params[0],
-        offset: data.params[2] || 0,
-        filter: null,
-      });
-    }
-
-    _handleTagListFilterOffsetRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.REPO,
-        detail: Gerrit.Nav.RepoDetailView.TAGS,
-        repo: data.params.repo,
-        offset: data.params.offset,
-        filter: data.params.filter,
-      });
-    }
-
-    _handleTagListFilterRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.REPO,
-        detail: Gerrit.Nav.RepoDetailView.TAGS,
-        repo: data.params.repo,
-        filter: data.params.filter || null,
-      });
-    }
-
-    _handleRepoListOffsetRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-repo-list',
-        offset: data.params[1] || 0,
-        filter: null,
-        openCreateModal: data.hash === 'create',
-      });
-    }
-
-    _handleRepoListFilterOffsetRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-repo-list',
-        offset: data.params.offset,
-        filter: data.params.filter,
-      });
-    }
-
-    _handleRepoListFilterRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-repo-list',
-        filter: data.params.filter || null,
-      });
-    }
-
-    _handleCreateProjectRoute(data) {
-      // Redirects the legacy route to the new route, which displays the project
-      // list with a hash 'create'.
-      this._redirect('/admin/repos#create');
-    }
-
-    _handleCreateGroupRoute(data) {
-      // Redirects the legacy route to the new route, which displays the group
-      // list with a hash 'create'.
-      this._redirect('/admin/groups#create');
-    }
-
-    _handleRepoRoute(data) {
-      const repo = data.params[0];
-      this._setParams({
-        view: Gerrit.Nav.View.REPO,
-        repo,
-      });
-      this.$.reporting.setRepoName(repo);
-    }
-
-    _handlePluginListOffsetRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-plugin-list',
-        offset: data.params[1] || 0,
-        filter: null,
-      });
-    }
-
-    _handlePluginListFilterOffsetRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-plugin-list',
-        offset: data.params.offset,
-        filter: data.params.filter,
-      });
-    }
-
-    _handlePluginListFilterRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-plugin-list',
-        filter: data.params.filter || null,
-      });
-    }
-
-    _handlePluginListRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.ADMIN,
-        adminView: 'gr-plugin-list',
-      });
-    }
-
-    _handleQueryRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.SEARCH,
-        query: data.params[0],
-        offset: data.params[2],
-      });
-    }
-
-    _handleQueryLegacySuffixRoute(ctx) {
-      this._redirect(ctx.path.replace(LEGACY_QUERY_SUFFIX_PATTERN, ''));
-    }
-
-    _handleChangeNumberLegacyRoute(ctx) {
-      this._redirect('/c/' + encodeURIComponent(ctx.params[0]));
-    }
-
-    _handleChangeRoute(ctx) {
-      // Parameter order is based on the regex group number matched.
-      const params = {
-        project: ctx.params[0],
-        changeNum: ctx.params[1],
-        basePatchNum: ctx.params[4],
-        patchNum: ctx.params[6],
-        view: Gerrit.Nav.View.CHANGE,
-      };
-
-      this.$.reporting.setRepoName(params.project);
-      this._redirectOrNavigate(params);
-    }
-
-    _handleDiffRoute(ctx) {
-      // Parameter order is based on the regex group number matched.
-      const params = {
-        project: ctx.params[0],
-        changeNum: ctx.params[1],
-        basePatchNum: ctx.params[4],
-        patchNum: ctx.params[6],
-        path: ctx.params[8],
-        view: Gerrit.Nav.View.DIFF,
-      };
-
-      const address = this._parseLineAddress(ctx.hash);
-      if (address) {
-        params.leftSide = address.leftSide;
-        params.lineNum = address.lineNum;
-      }
-      this.$.reporting.setRepoName(params.project);
-      this._redirectOrNavigate(params);
-    }
-
-    _handleChangeLegacyRoute(ctx) {
-      // Parameter order is based on the regex group number matched.
-      const params = {
-        changeNum: ctx.params[0],
-        basePatchNum: ctx.params[3],
-        patchNum: ctx.params[5],
-        view: Gerrit.Nav.View.CHANGE,
-        querystring: ctx.querystring,
-      };
-
-      this._normalizeLegacyRouteParams(params);
-    }
-
-    _handleLegacyLinenum(ctx) {
-      this._redirect(ctx.path.replace(LEGACY_LINENUM_PATTERN, '#$1'));
-    }
-
-    _handleDiffLegacyRoute(ctx) {
-      // Parameter order is based on the regex group number matched.
-      const params = {
-        changeNum: ctx.params[0],
-        basePatchNum: ctx.params[2],
-        patchNum: ctx.params[4],
-        path: ctx.params[5],
-        view: Gerrit.Nav.View.DIFF,
-      };
-
-      const address = this._parseLineAddress(ctx.hash);
-      if (address) {
-        params.leftSide = address.leftSide;
-        params.lineNum = address.lineNum;
-      }
-
-      this._normalizeLegacyRouteParams(params);
-    }
-
-    _handleDiffEditRoute(ctx) {
-      // Parameter order is based on the regex group number matched.
-      const project = ctx.params[0];
-      this._redirectOrNavigate({
-        project,
-        changeNum: ctx.params[1],
-        patchNum: ctx.params[2],
-        path: ctx.params[3],
-        lineNum: ctx.hash,
-        view: Gerrit.Nav.View.EDIT,
-      });
-      this.$.reporting.setRepoName(project);
-    }
-
-    _handleChangeEditRoute(ctx) {
-      // Parameter order is based on the regex group number matched.
-      const project = ctx.params[0];
-      this._redirectOrNavigate({
-        project,
-        changeNum: ctx.params[1],
-        patchNum: ctx.params[3],
-        view: Gerrit.Nav.View.CHANGE,
-        edit: true,
-      });
-      this.$.reporting.setRepoName(project);
-    }
-
-    /**
-     * Normalize the patch range params for a the change or diff view and
-     * redirect if URL upgrade is needed.
-     */
-    _redirectOrNavigate(params) {
-      const needsRedirect = this._normalizePatchRangeParams(params);
-      if (needsRedirect) {
-        this._redirect(this._generateUrl(params));
-      } else {
-        this._setParams(params);
+  _handleProjectsOldRoute(data) {
+    let params = '';
+    if (data.params[1]) {
+      params = encodeURIComponent(data.params[1]);
+      if (data.params[1].includes(',')) {
+        params =
+            encodeURIComponent(data.params[1]).replace('%2C', ',');
       }
     }
 
-    _handleAgreementsRoute() {
-      this._redirect('/settings/#Agreements');
+    this._redirect(`/admin/repos/${params}`);
+  }
+
+  _handleRepoCommandsRoute(data) {
+    const repo = data.params[0];
+    this._setParams({
+      view: Gerrit.Nav.View.REPO,
+      detail: Gerrit.Nav.RepoDetailView.COMMANDS,
+      repo,
+    });
+    this.$.reporting.setRepoName(repo);
+  }
+
+  _handleRepoAccessRoute(data) {
+    const repo = data.params[0];
+    this._setParams({
+      view: Gerrit.Nav.View.REPO,
+      detail: Gerrit.Nav.RepoDetailView.ACCESS,
+      repo,
+    });
+    this.$.reporting.setRepoName(repo);
+  }
+
+  _handleRepoDashboardsRoute(data) {
+    const repo = data.params[0];
+    this._setParams({
+      view: Gerrit.Nav.View.REPO,
+      detail: Gerrit.Nav.RepoDetailView.DASHBOARDS,
+      repo,
+    });
+    this.$.reporting.setRepoName(repo);
+  }
+
+  _handleBranchListOffsetRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.REPO,
+      detail: Gerrit.Nav.RepoDetailView.BRANCHES,
+      repo: data.params[0],
+      offset: data.params[2] || 0,
+      filter: null,
+    });
+  }
+
+  _handleBranchListFilterOffsetRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.REPO,
+      detail: Gerrit.Nav.RepoDetailView.BRANCHES,
+      repo: data.params.repo,
+      offset: data.params.offset,
+      filter: data.params.filter,
+    });
+  }
+
+  _handleBranchListFilterRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.REPO,
+      detail: Gerrit.Nav.RepoDetailView.BRANCHES,
+      repo: data.params.repo,
+      filter: data.params.filter || null,
+    });
+  }
+
+  _handleTagListOffsetRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.REPO,
+      detail: Gerrit.Nav.RepoDetailView.TAGS,
+      repo: data.params[0],
+      offset: data.params[2] || 0,
+      filter: null,
+    });
+  }
+
+  _handleTagListFilterOffsetRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.REPO,
+      detail: Gerrit.Nav.RepoDetailView.TAGS,
+      repo: data.params.repo,
+      offset: data.params.offset,
+      filter: data.params.filter,
+    });
+  }
+
+  _handleTagListFilterRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.REPO,
+      detail: Gerrit.Nav.RepoDetailView.TAGS,
+      repo: data.params.repo,
+      filter: data.params.filter || null,
+    });
+  }
+
+  _handleRepoListOffsetRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.ADMIN,
+      adminView: 'gr-repo-list',
+      offset: data.params[1] || 0,
+      filter: null,
+      openCreateModal: data.hash === 'create',
+    });
+  }
+
+  _handleRepoListFilterOffsetRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.ADMIN,
+      adminView: 'gr-repo-list',
+      offset: data.params.offset,
+      filter: data.params.filter,
+    });
+  }
+
+  _handleRepoListFilterRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.ADMIN,
+      adminView: 'gr-repo-list',
+      filter: data.params.filter || null,
+    });
+  }
+
+  _handleCreateProjectRoute(data) {
+    // Redirects the legacy route to the new route, which displays the project
+    // list with a hash 'create'.
+    this._redirect('/admin/repos#create');
+  }
+
+  _handleCreateGroupRoute(data) {
+    // Redirects the legacy route to the new route, which displays the group
+    // list with a hash 'create'.
+    this._redirect('/admin/groups#create');
+  }
+
+  _handleRepoRoute(data) {
+    const repo = data.params[0];
+    this._setParams({
+      view: Gerrit.Nav.View.REPO,
+      repo,
+    });
+    this.$.reporting.setRepoName(repo);
+  }
+
+  _handlePluginListOffsetRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.ADMIN,
+      adminView: 'gr-plugin-list',
+      offset: data.params[1] || 0,
+      filter: null,
+    });
+  }
+
+  _handlePluginListFilterOffsetRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.ADMIN,
+      adminView: 'gr-plugin-list',
+      offset: data.params.offset,
+      filter: data.params.filter,
+    });
+  }
+
+  _handlePluginListFilterRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.ADMIN,
+      adminView: 'gr-plugin-list',
+      filter: data.params.filter || null,
+    });
+  }
+
+  _handlePluginListRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.ADMIN,
+      adminView: 'gr-plugin-list',
+    });
+  }
+
+  _handleQueryRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.SEARCH,
+      query: data.params[0],
+      offset: data.params[2],
+    });
+  }
+
+  _handleQueryLegacySuffixRoute(ctx) {
+    this._redirect(ctx.path.replace(LEGACY_QUERY_SUFFIX_PATTERN, ''));
+  }
+
+  _handleChangeNumberLegacyRoute(ctx) {
+    this._redirect('/c/' + encodeURIComponent(ctx.params[0]));
+  }
+
+  _handleChangeRoute(ctx) {
+    // Parameter order is based on the regex group number matched.
+    const params = {
+      project: ctx.params[0],
+      changeNum: ctx.params[1],
+      basePatchNum: ctx.params[4],
+      patchNum: ctx.params[6],
+      view: Gerrit.Nav.View.CHANGE,
+    };
+
+    this.$.reporting.setRepoName(params.project);
+    this._redirectOrNavigate(params);
+  }
+
+  _handleDiffRoute(ctx) {
+    // Parameter order is based on the regex group number matched.
+    const params = {
+      project: ctx.params[0],
+      changeNum: ctx.params[1],
+      basePatchNum: ctx.params[4],
+      patchNum: ctx.params[6],
+      path: ctx.params[8],
+      view: Gerrit.Nav.View.DIFF,
+    };
+
+    const address = this._parseLineAddress(ctx.hash);
+    if (address) {
+      params.leftSide = address.leftSide;
+      params.lineNum = address.lineNum;
+    }
+    this.$.reporting.setRepoName(params.project);
+    this._redirectOrNavigate(params);
+  }
+
+  _handleChangeLegacyRoute(ctx) {
+    // Parameter order is based on the regex group number matched.
+    const params = {
+      changeNum: ctx.params[0],
+      basePatchNum: ctx.params[3],
+      patchNum: ctx.params[5],
+      view: Gerrit.Nav.View.CHANGE,
+      querystring: ctx.querystring,
+    };
+
+    this._normalizeLegacyRouteParams(params);
+  }
+
+  _handleLegacyLinenum(ctx) {
+    this._redirect(ctx.path.replace(LEGACY_LINENUM_PATTERN, '#$1'));
+  }
+
+  _handleDiffLegacyRoute(ctx) {
+    // Parameter order is based on the regex group number matched.
+    const params = {
+      changeNum: ctx.params[0],
+      basePatchNum: ctx.params[2],
+      patchNum: ctx.params[4],
+      path: ctx.params[5],
+      view: Gerrit.Nav.View.DIFF,
+    };
+
+    const address = this._parseLineAddress(ctx.hash);
+    if (address) {
+      params.leftSide = address.leftSide;
+      params.lineNum = address.lineNum;
     }
 
-    _handleNewAgreementsRoute(data) {
-      data.params.view = Gerrit.Nav.View.AGREEMENTS;
-      this._setParams(data.params);
-    }
+    this._normalizeLegacyRouteParams(params);
+  }
 
-    _handleSettingsLegacyRoute(data) {
-      // email tokens may contain '+' but no space.
-      // The parameter parsing replaces all '+' with a space,
-      // undo that to have valid tokens.
-      const token = data.params[0].replace(/ /g, '+');
-      this._setParams({
-        view: Gerrit.Nav.View.SETTINGS,
-        emailToken: token,
-      });
-    }
+  _handleDiffEditRoute(ctx) {
+    // Parameter order is based on the regex group number matched.
+    const project = ctx.params[0];
+    this._redirectOrNavigate({
+      project,
+      changeNum: ctx.params[1],
+      patchNum: ctx.params[2],
+      path: ctx.params[3],
+      lineNum: ctx.hash,
+      view: Gerrit.Nav.View.EDIT,
+    });
+    this.$.reporting.setRepoName(project);
+  }
 
-    _handleSettingsRoute(data) {
-      this._setParams({view: Gerrit.Nav.View.SETTINGS});
-    }
+  _handleChangeEditRoute(ctx) {
+    // Parameter order is based on the regex group number matched.
+    const project = ctx.params[0];
+    this._redirectOrNavigate({
+      project,
+      changeNum: ctx.params[1],
+      patchNum: ctx.params[3],
+      view: Gerrit.Nav.View.CHANGE,
+      edit: true,
+    });
+    this.$.reporting.setRepoName(project);
+  }
 
-    _handleRegisterRoute(ctx) {
-      this._setParams({justRegistered: true});
-      let path = ctx.params[0] || '/';
-
-      // Prevent redirect looping.
-      if (path.startsWith('/register')) { path = '/'; }
-
-      if (path[0] !== '/') { return; }
-      this._redirect(this.getBaseUrl() + path);
-    }
-
-    /**
-     * Handler for routes that should pass through the router and not be caught
-     * by the catchall _handleDefaultRoute handler.
-     */
-    _handlePassThroughRoute() {
-      location.reload();
-    }
-
-    /**
-     * URL may sometimes have /+/ encoded to / /.
-     * Context: Issue 6888, Issue 7100
-     */
-    _handleImproperlyEncodedPlusRoute(ctx) {
-      let hash = this._getHashFromCanonicalPath(ctx.canonicalPath);
-      if (hash.length) { hash = '#' + hash; }
-      this._redirect(`/c/${ctx.params[0]}/+/${ctx.params[1]}${hash}`);
-    }
-
-    _handlePluginScreen(ctx) {
-      const view = Gerrit.Nav.View.PLUGIN_SCREEN;
-      const plugin = ctx.params[0];
-      const screen = ctx.params[1];
-      this._setParams({view, plugin, screen});
-    }
-
-    _handleDocumentationSearchRoute(data) {
-      this._setParams({
-        view: Gerrit.Nav.View.DOCUMENTATION_SEARCH,
-        filter: data.params.filter || null,
-      });
-    }
-
-    _handleDocumentationSearchRedirectRoute(data) {
-      this._redirect('/Documentation/q/filter:' +
-          encodeURIComponent(data.params[0]));
-    }
-
-    _handleDocumentationRedirectRoute(data) {
-      if (data.params[1]) {
-        location.reload();
-      } else {
-        // Redirect /Documentation to /Documentation/index.html
-        this._redirect('/Documentation/index.html');
-      }
-    }
-
-    /**
-     * Catchall route for when no other route is matched.
-     */
-    _handleDefaultRoute() {
-      if (this._isInitialLoad) {
-        // Server recognized this route as polygerrit, so we show 404.
-        this._show404();
-      } else {
-        // Route can be recognized by server, so we pass it to server.
-        this._handlePassThroughRoute();
-      }
-    }
-
-    _show404() {
-      // Note: the app's 404 display is tightly-coupled with catching 404
-      // network responses, so we simulate a 404 response status to display it.
-      // TODO: Decouple the gr-app error view from network responses.
-      this._appElement().dispatchEvent(new CustomEvent('page-error',
-          {detail: {response: {status: 404}}}));
+  /**
+   * Normalize the patch range params for a the change or diff view and
+   * redirect if URL upgrade is needed.
+   */
+  _redirectOrNavigate(params) {
+    const needsRedirect = this._normalizePatchRangeParams(params);
+    if (needsRedirect) {
+      this._redirect(this._generateUrl(params));
+    } else {
+      this._setParams(params);
     }
   }
 
-  customElements.define(GrRouter.is, GrRouter);
-})();
+  _handleAgreementsRoute() {
+    this._redirect('/settings/#Agreements');
+  }
+
+  _handleNewAgreementsRoute(data) {
+    data.params.view = Gerrit.Nav.View.AGREEMENTS;
+    this._setParams(data.params);
+  }
+
+  _handleSettingsLegacyRoute(data) {
+    // email tokens may contain '+' but no space.
+    // The parameter parsing replaces all '+' with a space,
+    // undo that to have valid tokens.
+    const token = data.params[0].replace(/ /g, '+');
+    this._setParams({
+      view: Gerrit.Nav.View.SETTINGS,
+      emailToken: token,
+    });
+  }
+
+  _handleSettingsRoute(data) {
+    this._setParams({view: Gerrit.Nav.View.SETTINGS});
+  }
+
+  _handleRegisterRoute(ctx) {
+    this._setParams({justRegistered: true});
+    let path = ctx.params[0] || '/';
+
+    // Prevent redirect looping.
+    if (path.startsWith('/register')) { path = '/'; }
+
+    if (path[0] !== '/') { return; }
+    this._redirect(this.getBaseUrl() + path);
+  }
+
+  /**
+   * Handler for routes that should pass through the router and not be caught
+   * by the catchall _handleDefaultRoute handler.
+   */
+  _handlePassThroughRoute() {
+    location.reload();
+  }
+
+  /**
+   * URL may sometimes have /+/ encoded to / /.
+   * Context: Issue 6888, Issue 7100
+   */
+  _handleImproperlyEncodedPlusRoute(ctx) {
+    let hash = this._getHashFromCanonicalPath(ctx.canonicalPath);
+    if (hash.length) { hash = '#' + hash; }
+    this._redirect(`/c/${ctx.params[0]}/+/${ctx.params[1]}${hash}`);
+  }
+
+  _handlePluginScreen(ctx) {
+    const view = Gerrit.Nav.View.PLUGIN_SCREEN;
+    const plugin = ctx.params[0];
+    const screen = ctx.params[1];
+    this._setParams({view, plugin, screen});
+  }
+
+  _handleDocumentationSearchRoute(data) {
+    this._setParams({
+      view: Gerrit.Nav.View.DOCUMENTATION_SEARCH,
+      filter: data.params.filter || null,
+    });
+  }
+
+  _handleDocumentationSearchRedirectRoute(data) {
+    this._redirect('/Documentation/q/filter:' +
+        encodeURIComponent(data.params[0]));
+  }
+
+  _handleDocumentationRedirectRoute(data) {
+    if (data.params[1]) {
+      location.reload();
+    } else {
+      // Redirect /Documentation to /Documentation/index.html
+      this._redirect('/Documentation/index.html');
+    }
+  }
+
+  /**
+   * Catchall route for when no other route is matched.
+   */
+  _handleDefaultRoute() {
+    if (this._isInitialLoad) {
+      // Server recognized this route as polygerrit, so we show 404.
+      this._show404();
+    } else {
+      // Route can be recognized by server, so we pass it to server.
+      this._handlePassThroughRoute();
+    }
+  }
+
+  _show404() {
+    // Note: the app's 404 display is tightly-coupled with catching 404
+    // network responses, so we simulate a 404 response status to display it.
+    // TODO: Decouple the gr-app error view from network responses.
+    this._appElement().dispatchEvent(new CustomEvent('page-error',
+        {detail: {response: {status: 404}}}));
+  }
+}
+
+customElements.define(GrRouter.is, GrRouter);