ef77b08098f12682c3318243d14dfb52cda203f8
angie
  Mon Apr 25 11:50:43 2016 -0700
Cath's suggestion from #15277#note-190: Use the same width for the select assembly menu as for the position input and center the Go button vertically.  Also fixed IE8 clobbering of activeGenomes global.  refs #15277

diff --git src/hg/js/hgGateway.js src/hg/js/hgGateway.js
index 3e4181e..f3e9801 100644
--- src/hg/js/hgGateway.js
+++ src/hg/js/hgGateway.js
@@ -1,1604 +1,1616 @@
 // hgGateway - autocomplete + graphical interface to select species, assembly & position.
 
 // Copyright (C) 2016 The Regents of the University of California
 
 // Several modules are defined in this file -- if some other code needs them someday,
 // they can be moved out to lib files.
 
 // function svgCreateEl: convenience function for creating new SVG elements, used by
 // speciesTree and rainbow modules.
 
 // speciesTree: module that exports draw() function for drawing a phylogenetic tree
 // (and list of hubs above the tree, if any) in a pre-existing SVG element -- see
 // hg/hgGateway/hgGateway.html.
 
 // rainbow: module that exports draw() function and colors.  draw() adds stripes using
 // a spectrum of colors that are associated to species groups.  The hgGateway view code
 // uses coordinates of stripes within the tree image to create a corresponding "rainbow"
 // slider bar to the left of the phylogenetic tree container.
 
 // autocompleteCat: customized JQuery autocomplete plugin that includes watermark and
 // can display results broken down by category (for example, genomes from various
 // assembly hubs and native genomes).
 
 // hgGateway: module of mostly view/controller code (model state comes directly from server).
 
-// Globals:
-/* globals calculateHgTracksWidth */ // pragma for jshint; function is defined in utils.js
-var dbDbTree = dbDbTree || undefined;
-var activeGenomes = activeGenomes || undefined;
-var cart = cart || undefined;
+// Globals (pragma for jshint):
+/* globals dbDbTree, activeGenomes, cart */
+/* globals calculateHgTracksWidth */ // function is defined in utils.js
+
 
 function svgCreateEl(type, config) {
     // Helper function for creating a new SVG element and initializing its
     // properties and attributes.  Type is something like 'rect', 'text', 'g', etc;
     // config is an object like { id: 'newThingie', x: 0, y: 10, title: 'blah blah' }.
     var svgns = 'http://www.w3.org/2000/svg';
     var xlinkns = 'http://www.w3.org/1999/xlink';
     var el = document.createElementNS(svgns, type);
     var title, titleEl;
     if (el) {
         _.forEach(config, function(value, setting) {
             if (setting === 'textContent') {
                 // Text content (the text in a text element or title element) is a property:
                 el.textContent = value;
             } else if (setting === 'href') {
                 // href comes from a different namespace so must use setAttributeNS:
                 el.setAttributeNS(xlinkns, 'href', value);
             } else if (setting === 'title') {
                 title = value;
             } else if (setting === 'className') {
                 el.setAttribute('class', value);
             } else {
                 // Most of the time we're just setting an attribute:
                 el.setAttribute(setting, value);
             }
         });
     }
     // Mouseover title actually requires creating a child element.
     // Strangely, if I did this in the above loop, the child element was lost if
     // props/attributes were set afterwards!!  So save title for last.
     if (title) {
         titleEl = document.createElementNS(svgns, 'title');
         titleEl.textContent = title;
         el.appendChild(titleEl);
     }
     return el;
 }
 
 ///////////////////////////// Module: speciesTree /////////////////////////////
 
 var speciesTree = (function() {
     // SVG phylogenetic tree of species in the browser (with connected assembly hubs above tree)
 
     // Layout parameters/configuration (object passed into draw can override these defaults):
     var cfg = { labelRightX: 230,
                 labelStartY: 18,
                 speciesLineOffsetX: 8,
                 branchLength: 5,
                 halfTextHeight: 4,
                 hubLineOffset: 100,
                 labelLineHeight: 18,
                 paddingRight: 5,
                 paddingBottom: 5,
                 branchPadding: 2,
                 onClickSpeciesName: 'console.log',
                 onClickHubName:     'console.log',
                 trackHubsUrl: '',
                 containerWidth: 370
               };
 
     function checkTree(node) {
         // Return true if node and its descendants are of the form
         // Array[ label:String, taxId:Number, sciName:String, Array[ [node]...] ]
         // e.g. ['root', taxId0, sciName0,
         //       [ ['leaf1', taxId1, sciName1, []],
         //         ['leaf2', taxId2, sciName2, []] ]
         //      ]
         if (! _.isString(node[0])) {
             console.log('label is not a string', node);
             return false;
         }
         if (! _.isNumber(node[1])) {
             console.log('taxId is not a number', node);
             return false;
         }
         if (! (node[2] === null || _.isString(node[2]))) {
             console.log('sciName is not null or string', node);
             return false;
         }
         return _.every(node[3], checkTree);
     }
 
     function addDepth(node) {
         // Each node is of the form [ label, taxId, sciName, node[] ]
         // e.g. ['root', taxId0, sciName0,
         //       [ ['leaf1', taxId1, sciName1, []],
         //         ['leaf2', taxId2, sciName2, []] ]
         //      ]
         // Add a fifth property to node: depth, i.e. the maximum number of
         // branching nodes along any path from node to a leaf.
         // Returns depth.
         var kids = node[3];
         var depth, deepestKid;
         if (!kids || kids.length === 0) {
             // Leaf: depth is 0
             depth = 0;
         } else if (kids.length === 1) {
             // Node with one child: pass-through, depth is child's depth
             depth = addDepth(kids[0]);
         } else {
             // Node with multiple children: depth is 1 + max child depth
             deepestKid = _.max(kids, addDepth);
             depth = 1 + deepestKid[4];
         }
         node[4] = depth;
         return depth;
     }
 
     function addSpeciesLabel(svg, label, taxId, sciName, y) {
         // Add a species label to svg at y offset
         var onClickString = cfg.onClickSpeciesName + '(' + taxId + ')';
         var text = svgCreateEl('text', { x: cfg.labelRightX, y: y,
                                          name: 'textEl_' + taxId,
                                          title: sciName,
                                          textContent: label,
                                          onclick: onClickString });
         svg.appendChild(text);
     }
 
     function addLine(svg, x1, y1, x2, y2, mouseover) {
         // Add <line x1=x1, y1=y1, x2=x2, y2=y2 /> optionally with mouseover title
         var config = { x1: x1, y1: y1,
                        x2: x2, y2: y2,
                        title: mouseover };
         var line;
         if (x1 === x2) {
             // vertical lines get special styling
             config.className = 'vert';
         }
         line = svgCreateEl('line', config);
         svg.appendChild(line);
     }
 
     function calcBranchX(kidRetList, depth, parentDepth, parentNodeDepth) {
         // Return the x offset used for drawing lines from children and a vertical line
         // connecting them.
         // If the branch from parent is longer than the minimum length, we can scale myX
         // to reflect how many nodes are skipped in branches to the right vs. to the left,
         // ensuring the minimum branch length on either side.  Without this adjustment,
         // we sometimes get very long branches that skip only one node to the right of short
         // branches that skip many nodes.
         var myX;
         var kidMaxX = _.max(kidRetList, 'x').x;
         var branchSteps = parentDepth - depth;
         var kidMinNodeDepth, ratioSkipped, stepsX;
         if (branchSteps > 1) {
             kidMinNodeDepth = _.min(kidRetList, 'nodeDepth').nodeDepth;
             ratioSkipped = kidMinNodeDepth / (kidMinNodeDepth + parentNodeDepth);
             stepsX = (branchSteps+1) * ratioSkipped;
             // Make sure there is at least one step (min branch width) before and after
             // stepsX.
             if (stepsX < 1) {
                 stepsX = 1;
             } else if (stepsX > branchSteps) {
                 stepsX = branchSteps;
             }
             myX = kidMaxX + cfg.branchLength * stepsX;
         } else {
             // Only one change in depth level, so short branch:
             myX = kidMaxX + cfg.branchLength;
         }
         return myX;
     }
 
     function drawNode(svg, node, leafY, leafTops, parentDepth, parentNodeDepth) {
         // Each node is of the form [ label, taxId, sciName, node[], depth ]
         // e.g. ['root', taxId0, sciName0,
         //       [ ['leaf1', taxId1, sciName1, [], 0],
         //         ['leaf2', taxId2, sciName2, [], 0] ],
         //       1,
         //      ]
         // depth is the max number of branching nodes under this node.
         // Recursively draw the tree by generating new SVG elements and adding them to svg.
         // Returns  {x:, y:, leafY:} where x and y are the endpoint for the parent's branch
         // to this node and leafY is the Y offset for the next leaf label.
         // For leaf nodes, draw labels, store y in leafTops, and return label coordinates.
         // Nodes with a single child don't draw anything, they simply return the
         // child's info.
         // Nodes with multiple children draw a horizontal line to each child and
         // a vertical line connecting the horizontal lines.  They return the {X,Y} of
         // the midpoint of the vertical line.
         // optional arg parentDepth is the depth of the parent node as defined above.
         // optional arg parentNodeDepth is 1 plus the number of single-child nodes that were
         // skipped on the way from the last branching ancestor.
         // For internal use, the return object also includes nodeDepth: 1 + number of
         // nodes skipped between this node and the next branching descendant or leaf
         // and mouseover: horizontal line labels showing child label plus any skipped
         // nodes' labels.
         var label = node[0], taxId = node[1], sciName = node[2], kids = node[3], depth = node[4];
         var myX, myY;
         var kidRet, kidRetList, kidMinY, kidMaxY, extraSpace;
         parentDepth = parentDepth || 1;
         parentNodeDepth = parentNodeDepth || 1;
         if (!kids || kids.length === 0) {
             // leaf node: draw species label, store myY in leafTops
             addSpeciesLabel(svg, label, taxId, sciName, leafY);
             myX = cfg.labelRightX + cfg.speciesLineOffsetX;
             myY = leafY - cfg.halfTextHeight;
             leafTops[label] = leafY - cfg.labelLineHeight;
             return { x: myX, y: myY, leafY: leafY + cfg.labelLineHeight, nodeDepth: 1 };
         } else if (kids.length === 1) {
             // Single child: don't draw anything (keep the rendering compact),
             // but make a note that we skipped a node so we can use it for mouseover text
             kidRet = drawNode(svg, kids[0], leafY, leafTops, parentDepth, parentNodeDepth + 1);
             kidRet.nodeDepth++;
             if (kidRet.mouseover) {
                 kidRet.mouseover += ' - ' + label;
             } else {
                 kidRet.mouseover = label;
             }
             return kidRet;
         } else {
             // Multiple children.  First pass to draw kids and gather their coords,
             // second pass to draw lines connecting kids.
             extraSpace = (depth - 1) * cfg.branchPadding;
             kidRetList = _.map(kids, function(kid) {
                 var kidRet = drawNode(svg, kid, leafY, leafTops, depth, 1);
                 if (! kidRet.mouseover) {
                     // If nothing was skipped, use kid's sciName / label (same as kid's vert line)
                     var kidLabel = kid[0], kidSciName = kid[2];
                     kidRet.mouseover = kidSciName ? kidSciName : kidLabel;
                 }
                 leafY = kidRet.leafY + extraSpace;
                 return kidRet;
             });
             myX = calcBranchX(kidRetList, depth, parentDepth, parentNodeDepth);
             // Draw horizontal lines from kids
             _.forEach(kidRetList, function(kidRet) {
                 addLine(svg, kidRet.x, kidRet.y, myX, kidRet.y, kidRet.mouseover);
             });
             // Draw vertical line connecting kids
             kidMinY = kidRetList[0].y;
             kidMaxY = kidRetList[kids.length-1].y;
             addLine(svg, myX, kidMinY, myX, kidMaxY, label);
             myY = (kidMinY + kidMaxY) / 2;
             return { x: myX, y: myY, leafY: leafY, nodeDepth: 1, mouseover: label };
         }
     }
 
     function addTrackHubsLink(svg, x, y) {
         // Add a label with link to hgHubConnect at the given position.
         var a = svgCreateEl('a', { 'href': cfg.hgHubConnectUrl });
         var text = svgCreateEl('text', { x: x, y: y,
                                          textContent: 'Hub Genomes',
                                          className: 'trackHubsLink' });
         a.appendChild(text);
         svg.appendChild(a);
     }
 
     function doubleQuote(string) {
         return '"' + string + '"';
     }
 
     function addHubLabel(svg, hub, y) {
         // Add a track hub label to svg at y offset
         var label = hub.shortLabel + ' (' + hub.assemblyCount + ')';
         // There are a bunch of hub properties to pass to the onClick handler;
         // too bad we can't pass a bound function but instead must build a string:
         var onClickString = cfg.onClickHubName + '(' +
                             doubleQuote(hub.hubUrl) + ', ' +
                             hub.taxId + ', ' +
                             doubleQuote(hub.defaultDb) + ', ' +
                             doubleQuote(hub.name) +
                             ')';
         var text = svgCreateEl('text', { x: cfg.labelRightX, y: y,
                                          textContent: label,
                                          name: 'textEl_' + hub.name,
                                          onclick: onClickString,
                                          title: hub.longLabel });
         svg.appendChild(text);
     }
 
     function drawHubs(svg, hubList, yIn) {
         // Add a label for each hub in hubList, with a separator line below and
         // "Hub Genomes" link instead of a tree.
         var y = yIn;
         var hub, i, textX, textY, lineX1, lineY, lineX2;
         if (hubList && hubList.length) {
             for (i = 0;  i < hubList.length;  i++) {
                 hub = hubList[i];
                 addHubLabel(svg, hub, y);
                 y += cfg.labelLineHeight;
             }
             textX = cfg.labelRightX + cfg.speciesLineOffsetX;
             textY = (yIn + y - cfg.labelLineHeight) / 2;
             addTrackHubsLink(svg, textX, textY);
             lineX1 = cfg.hubLineOffset;
             lineY = y - cfg.halfTextHeight;
             lineX2 = cfg.containerWidth - cfg.speciesLineOffsetX;
             addLine(svg, lineX1, lineY, lineX2, lineY);
             y += cfg.labelLineHeight;
         }
         return y;
     }
 
     function draw(svg, dbDbTree, hubList, cfgOverrides) {
         // dbDbTree is the root node of a phylogenetic tree that we render in svg.
         // cfgOverrides should be used to provide (names of) meaningful onclick functions
         // and track hub URL.
         // Return the width and height of the tree, the top offset of the tree (below hubs
         // if any), and an object with the top coords of each label/leaf.
         // Instead of tacking a bunch of children directly onto svg, make a <g> (group)
         // and append that to svg when done.
         // First see if there's already something there that we will replace:
         var oldG = svg.getElementById('hubsAndTree');
         var newG = svgCreateEl('g', { id: 'hubsAndTree' });
         // y offsets of tops of species labels (leaves of dbDbTree), filled in by drawNode.
         var leafTops = {};
         var hubBottomY, treeInfo, width, height;
         if (! checkTree(dbDbTree)) {
             console.error('dbDbTree in wrong format', dbDbTree);
             return;
         }
         _.assign(cfg, cfgOverrides);
         hubBottomY = drawHubs(newG, hubList, cfg.labelStartY);
         addDepth(dbDbTree);
         treeInfo = drawNode(newG, dbDbTree, hubBottomY, leafTops);
         width = treeInfo.x + cfg.paddingRight;
         height = treeInfo.leafY - cfg.labelLineHeight + cfg.paddingBottom;
         if (oldG) {
             svg.removeChild(oldG);
         }
         svg.appendChild(newG);
         return { width: width, height: height,
                  yTree: hubBottomY - cfg.labelLineHeight,
                  leafTops: leafTops };
     }
 
     return { draw: draw };
 
 }()); // speciesTree
 
 
 ///////////////////////////// Module: rainbow /////////////////////////////
 
 var rainbow = (function() {
     // Add rainbow stripes with cute species icons on the left of an SVG element that
     // already has a phylogenetic tree drawn by speciesTree.
 
     // Layout parameters/configuration (object passed into draw can override these defaults):
     var cfg = { stripeWidth: 69,
                 iconX: 2,
                 iconYOffset: -14,
                 iconWidth: 65,
                 iconHeight: 65,
                 iconSpriteUrl: '../images/jWestIconsAlpha65px.png',
                 iconSpriteWidth: 325,
                 iconSpriteHeight: 325
                 };
 
     // Color spectrum for slider and tree display:
     var stripeColors = [ '#7E1F16',
                          '#A12321',
                          '#C15026',
                          '#DF6933',
                          '#EB8734',
                          '#B57E2A',
                          '#CD9C2A',
                          '#CFB32B',
                          '#959E38',
                          '#3A8349',
                          '#216D6D',
                          '#4C749B',
                          '#31469A',
                          '#7E4475',
                          '#231F1F' ];
 
     // Hubs go above the species rainbow -- give them their own color of stripe:
     var hubColor = '#60180C';
 
     // Taxonomy IDs for assigning rainbow stripes to species groups:
     var stripeTaxIds = [ 9443,   // Primates
                          314146, // Euarchontoglires
                          9362,   // Insectivora
                          91561,  // Cetartiodactyla
                          314145, // Laurasiatheria
                          9397,   // Chiroptera
                          9347,   // Eutheria
                          40674,  // Mammalia
                          32523,  // Tetrapoda
                          7742,   // Vertebrata
                          33511,  // Deuterostomia
                          33392,  // Endopterygota
                          6072,   // Eumetazoa
                          2759,   // Eukaryota
                          1 ];    // root
 
     // Cute species icons placed along the stripe also help to orient.
     // This maps leaf labels to icon names:
     var iconSpeciesToName = { Human: 'Human',
                               Mouse: 'Mouse',
                               'D. melanogaster': 'Fly',
                               'C. elegans': 'Worm',
                               'S. cerevisiae': 'Yeast',
                               'Rhesus': 'Monkey',
                               Hedgehog: 'Hedgehog',
                               // Pig: 'Pig',
                               Cow: 'Cow',
                               'Killer whale': 'Orca',
                               Horse: 'Horse',
                               Dog: 'Dog',
                               'Pacific walrus': 'Walrus',
                               'Megabat': 'Bat',
                               Elephant: 'Elephant',
                               Manatee: 'Manatee',
                               Armadillo: 'Armadillo',
                               'Wallaby': 'Kangaroo',
                               'Zebra finch': 'Bird',
                               Lizard: 'Lizard',
                               'X. tropicalis': 'Frog',
                               'Fugu': 'Fish',
                               'Zaire ebolavirus': 'Ebola',  // on hgwdev April 2016
                               'Ebola virus': 'Ebola'        // on RR April 2016
                             };
 
     // The icon sprite image has 5 rows and 5 columns:
     var spriteRowCol = { Human: [0,0],
                          Mouse: [0,1],
                          Fly: [0,3],
                          Worm: [0,4],
                          Yeast: [1,0],
                          Monkey: [1,1],
                          Hedgehog: [1,2],
                          Pig: [1,3],
                          Cow: [1,4],
                          Orca: [2,0],
                          Horse: [2,1],
                          Dog: [2,2],
                          Walrus: [2,3],
                          Bat: [2,4],
                          Elephant: [3,0],
                          Manatee: [3,1],
                          Armadillo: [3,2],
                          Kangaroo: [3,3],
                          Bird: [3,4],
                          Lizard: [4,0],
                          Frog: [4,1],
                          Fish: [4,2],
                          Ebola: [4,3]
                        };
 
     // Some icon drawings are shorter than others, and some need to be moved up to make space
     // for close neighbors.
     var iconFudgeY = { Human: 20,
                          Mouse: 0,
                          Fly: 0,
                          Worm: 0,
                          Monkey: 0,
                          Hedgehog: -18,
                          Pig: -5,
                          Cow: 0,
                          Orca: 0,
                          Horse: -8,
                          Dog: 15,
                          Walrus: 40,
                          Bat: 0,
                          Elephant: -20,
                          Manatee: 20,
                          Armadillo: 0,
                          Kangaroo: 0,
                          Bird: 0,
                          Lizard: -20,
                          Frog: -10,
                          Fish: 0,
                          Yeast: -15,
                          Ebola: 0
                        };
 
     function findStripeTops(node, parentStripeIx, leafTops, stripeTops, yPrev) {
         // Each node is of the form [ label, taxId, sciName, node[], ... ]
         // Recursively find the top coordinate of each stripe in stripeTaxIds,
         // using node taxId.  Modifies stripeTops.  Returns the y of the top of the
         // last leaf visited.
         var label = node[0], taxId = node[1], kids = node[3];
         var stripeIx = stripeTaxIds.indexOf(taxId);
         var i;
         // Inherit parent stripe unless this node is found in stripeTaxIds:
         if (stripeIx < 0) {
             stripeIx = parentStripeIx;
         }
         if (!kids || kids.length === 0) {
             // leaf node: if this stripe's top coord has not yet been assigned,
             // assign it.
             if (stripeTops[stripeIx] === undefined) {
                 stripeTops[stripeIx] = (leafTops[label] + yPrev) / 2;
             }
             yPrev = leafTops[label];
         } else {
             // descend to children
             for (i = 0;  i < kids.length;  i++) {
                 yPrev = findStripeTops(kids[i], stripeIx, leafTops, stripeTops, yPrev);
             }
         }
         return yPrev;
     }
 
     function addRectFill(svg, x, y, width, height, color) {
         // Add filled rectangle to svg
         var rect = svgCreateEl('rect', { x: x, y: y,
                                          width: width, height: height,
                                          style: 'fill:' + color + '; stroke:' + color });
         svg.appendChild(rect);
     }
 
     function drawStripes(svg, dbDbTree, yTop, height, leafTops) {
         var stripeCount = stripeColors.length;
         var lastStripeIx = stripeCount - 1;
         var stripeTops = [];
         var i, stripeHeight;
         findStripeTops(dbDbTree, lastStripeIx, leafTops, stripeTops, yTop);
         // Add an extra "stripe" coord so we have the coord for the bottom of the last stripe:
         stripeTops[stripeCount] = height;
         // Initialize missing stripes to 0-height (top = next stripe's top), if any:
         for (i = stripeCount - 1;  i >= 0;  i--) {
             if (stripeTops[i] === undefined) {
                 console.warn("No species found for stripe " + i + ", taxId " + stripeTaxIds[i]);
                 stripeTops[i] = stripeTops[i+1];
             }
         }
         for (i = 0;  i < stripeCount;  i++) {
             stripeHeight = stripeTops[i+1] - stripeTops[i];
             addRectFill(svg, 0, stripeTops[i], cfg.stripeWidth, stripeHeight, stripeColors[i]);
         }
         // Add stripe for hubs, if any:
         if (yTop > 0) {
             addRectFill(svg, 0, 0, cfg.stripeWidth, yTop, hubColor);
         }
         return stripeTops;
     }
 
     function drawOneIcon(svg, name, y) {
         // Create an image, offset so that the icon is positioned where we need it,
         // and use a clip-path to limit display to just that icon, not the whole sprite image.
         var iconY = y + cfg.iconYOffset + iconFudgeY[name];
         var rowCol = spriteRowCol[name];
         var row = rowCol[0], column = rowCol[1];
         var clipPathId = 'clip' + name;
         var img = svgCreateEl('image', { x: cfg.iconX - (column * cfg.iconWidth),
                                          y: iconY - (row * cfg.iconHeight),
                                          width: cfg.iconSpriteWidth,
                                          height: cfg.iconSpriteHeight,
                                          style: 'clip-path: url(#' + clipPathId + ')',
                                          href: cfg.iconSpriteUrl });
         // Set the y of the pre-existing clip path:
         var rect = $('#' + clipPathId + ' rect')[0];
         rect.setAttribute('y', iconY);
         svg.appendChild(img);
     }
 
     function drawIcons(svg, leafTops) {
         // For each icon listed in iconSpeciesToName, look up the species' y offset in
         // the tree and add the icon to svg.
         _.forEach(iconSpeciesToName, function (name, species) {
             var y = leafTops[species];
             if (y >= 0) {
                 drawOneIcon(svg, name, y);
             }
         });
     }
 
     function draw(svg, dbDbTree, yTree, height, leafTops) {
         // Draw stripes with colors corresponding to species groups and cute-species icons.
         // Return y offsets of stripes so that a slider widget can be drawn accordingly.
         // Instead of tacking a bunch of children directly onto svg, make a <g> (group)
         // and append that to svg when done.
         // First see if there's already something there that we will replace:
         var oldG = svg.getElementById('stripesAndIcons');
         var newG = svgCreateEl('g', { id: 'stripesAndIcons' });
         var stripeTops = drawStripes(newG, dbDbTree, yTree, height, leafTops);
         drawIcons(newG, leafTops);
         if (oldG) {
             svg.removeChild(oldG);
         }
         svg.appendChild(newG);
         return stripeTops;
     }
 
     return { draw: draw,
              colors: stripeColors,
              hubColor: hubColor
              };
 }()); // rainbow
 
 
 ///////////////////////////// Module: autocompleteCat /////////////////////////////
 
 var autocompleteCat = (function() {
     // Customize jQuery UI autocomplete to show item categories and support html markup in labels.
     // Adapted from https://jqueryui.com/autocomplete/#categories and
     // http://forum.jquery.com/topic/using-html-in-autocomplete
     // Also adds watermarm to input.
     $.widget("custom.autocompleteCat",
              $.ui.autocomplete,
              {
                _renderMenu: function(ul, items) {
                    var that = this;
                    var currentCategory = "";
                    // There's no this._super as shown in the doc, so I can't override
                    // _create as shown in the doc -- just do this every time we render...
                    this.widget().menu("option", "items", "> :not(.ui-autocomplete-category)");
                    $.each(items,
                           function(index, item) {
                               // Add a heading each time we see a new category:
                               if (item.category && item.category !== currentCategory) {
                                   ul.append("<li class='ui-autocomplete-category'>" +
                                             item.category + "</li>" );
                                   currentCategory = item.category;
                               }
                               that._renderItem( ul, item );
                           });
                },
                _renderItem: function(ul, item) {
                  // In order to use HTML markup in the autocomplete, one has to overwrite
                  // autocomplete's _renderItem method using .html instead of .text.
                  // http://forum.jquery.com/topic/using-html-in-autocomplete
                    return $("<li></li>")
                        .data("item.autocomplete", item)
                        .append($("<a></a>").html(item.label))
                        .appendTo(ul);
                }
              });
 
     function init($input, options) {
         // Set up an autocomplete and watermark for $input, with a callback options.onSelect
         // for when the user chooses a result.
         // If options.baseUrl is null, the autocomplete will not do anything, but we (re)initialize
         // it anyway in case the same input had a previous db's autocomplete in effect.
         // options.onServerReply (if given) is a function (Array, term) -> Array that
         // post-processes the list of items returned by the server before the list is
         // passed back to autocomplete for rendering.
         // The following two options apply only when using our locally modified jquery-ui:
         // If options.enterSelectsIdentical is true, then if the user hits Enter in the text input
         // and their term has an exact match in the autocomplete results, that result is selected.
         // options.onEnterTerm (if provided) is a callback function (jqEvent, jqUi) invoked
         // when the user hits Enter, after handling enterSelectsIdentical.
 
         // The function closure allows us to keep a private cache of past searches.
         var cache = {};
 
         var doSearch = function(term, acCallback) {
             // Look up term in searchObj and by sending an ajax request
             var timestamp = new Date().getTime();
             var url = options.baseUrl + encodeURIComponent(term) + '&_=' + timestamp;
             $.getJSON(url)
                .done(function(results) {
                 if (_.isFunction(options.onServerReply)) {
                     results = options.onServerReply(results, term);
                 }
                 cache[term] = results;
                 acCallback(results);
             });
             // ignore errors to avoid spamming people on flaky network connections
             // with tons of error messages (#8816).
         };
 
         var autoCompleteSource = function(request, acCallback) {
             // This is a callback for jqueryui.autocomplete: when the user types
             // a character, this is called with the input value as request.term and an acCallback
             // for this to return the result to autocomplete.
             // See http://api.jqueryui.com/autocomplete/#option-source
             var results = cache[request.term];
             if (results) {
                 acCallback(results);
             } else if (options.baseUrl) {
                 doSearch(request.term, acCallback);
             }
         };
 
         var autoCompleteSelect = function(event, ui) {
             // This is a callback for autocomplete to let us know that the user selected
             // a term from the list.  See http://api.jqueryui.com/autocomplete/#event-select
             options.onSelect(ui.item);
             $input.blur();
         };
 
         // Provide default values where necessary:
         options.onSelect = options.onSelect || console.log;
         options.enterSelectsIdentical = options.enterSelectsIdentical || false;
 
         $input.autocompleteCat({
             delay: 500,
             minLength: 2,
             source: autoCompleteSource,
             select: autoCompleteSelect,
             enterSelectsIdentical: options.enterSelectsIdentical,
             enterTerm: options.onEnterTerm
         });
 
         if (options.watermark) {
             $input.Watermark(options.watermark, '#686868');
         }
     }
 
     return { init: init };
 }()); // autocompleteCat
 
 
 ///////////////////////////// Module: hgGateway /////////////////////////////
 
 var hgGateway = (function() {
     // Interactive parts of the new gateway page: species autocomplete,
     // graphical species-picker, db select, and position autocomplete.
 
     // Constants
     var speciesWatermark = 'Enter species or common name';
     var positionWatermark = 'Enter position, gene symbol or search terms';
     // Shortcuts to popular species:
     var favIconTaxId = [ ['Human', 9606],
                          ['Mouse', 10090],
                          ['Rat', 10116],
                          ['Fly', 7227],
                          ['Worm', 6239],
                          ['Yeast', 559292] ];
     // Aliases for species autocomplete:
     var commonToSciNames = { bats: 'Chiroptera',
                              bees: 'Apoidea',
                              birds: 'Aves',
                              fish: 'Actinopterygii',
                              fly: 'Diptera',
                              flies: 'Diptera',
                              frogs: 'Anura',
                              fruitfly: 'Drosophila',
                              'fruit fly': 'Drosophila',
                              honeybees: 'Apinae',
                              'honey bees': 'Apinae',
                              monkeys: 'Simiiformes',
                              mosquitos: 'Culicidae',
                              worms: 'Nematoda',
                              yeast: 'Ascomycota' };
 
     var getBetterBrowserMessage = '<P style="padding-left: 10px;">' +
                                   'Our website has detected that you are using ' +
                                   'an outdated browser that will prevent you from ' +
                                   'accessing certain features. An update is not ' +
                                   'required, but it is strongly recommended to ' +
                                   'improve your browsing experience. ' +
                                   'Please use the following links to upgrade your ' +
                                   'existing browser to ' +
                                   '<A HREF="https://www.mozilla.org/en-US/firefox/new/">' +
                                   ' FireFox</A> or ' +
                                   '<A HREF="https://www.google.com/chrome/browser/">' +
                                   'Chrome</A>.' +
                                   '</P>';
 
     // Globals
     // Set this to true to see server requests and responses in the console.
     var debugCartJson = false;
     // This is a global (within wrapper function scope) so event handlers can use it
     // without needing to bind functions.
     var scrollbarWidth = 0;
     // This holds everything we need to know to draw the page: taxId, db, hubs, description etc.
     var uiState = {};
+    // This is dbDbTree after pruning -- null if dbDbTree has no children left
+    var prunedDbDbTree;
 
     function setupFavIcons() {
         // Set up onclick handlers for shortcut buttons and labels
         var i, name, taxId, onClick;
         for (i = 0;  i < favIconTaxId.length;  i++) {
             name = favIconTaxId[i][0];
             taxId = favIconTaxId[i][1];
             // When user clicks on icon, set the taxId (default database);
             // scroll the image to that species and clear the species autocomplete input.
             onClick = setTaxId.bind(null, taxId, null, true, true);
             // Onclick for both the icon and its sibling label:
             $('.jwIconSprite' + name).parent().children().click(onClick);
         }
     }
 
     function addCategory(cat, item) {
         // Clone item, add category: cat to it and return it (helper function, see below).
         var clone = {};
         _.assign(clone, item, { category: cat });
         return clone;
     }
 
     function autocompleteFromNode(node) {
         // Traverse dbDbTree to make autocomplete result lists for all non-leaf node labels.
         // Returns an object mapping each label of node and descendants to a list of
         // result objects with the same structure that we'd get from a server request.
         if (! node) {
             return;
         }
         var searchObj = {};
         var myResults = [];
         var label = node[0], taxId = node[1], kids = node[3];
         var addMyLabel;
         if (!kids || kids.length === 0) {
             // leaf node: return autocomplete result for species
             myResults = [ { genome: label,
                             label: label,
                             taxId: taxId } ];
         } else {
             // Accumulate the list of all children's result lists;
             // keep each's child searchObj mappings unless the child is a leaf
             // (which would be redundant with server autocomplete results).
             addMyLabel = addCategory.bind(null, label);
             myResults = _.flatten(
                 _.map(kids, function(kid) {
                     var kidLabel = kid[0], kidKids = kid[3];
                     var kidObj = autocompleteFromNode(kid);
                     // Clone kid's result list and add own label as category:
                     var kidResults = _.map(kidObj[kidLabel], addMyLabel);
                     // Add kid's mappings to searchObj only if kid is not a leaf.
                     if (kidKids && kidKids.length > 0) {
                         _.assign(searchObj, kidObj);
                     }
                     return kidResults;
                 })
             );
         }
         // Exclude some overly broad categories:
         if (label !== 'root' && label !== 'cellular organisms') {
             searchObj[label] = myResults;
         }
         return searchObj;
     }
 
     function autocompleteFromTree(node, searchObj) {
         // Traverse dbDbTree to make autocomplete result lists for all non-leaf node labels.
         // searchObj is extended to map each label of node and descendants to a list of
         // result objects with the same structure that we'd get from a server request.
         _.assign(searchObj, autocompleteFromNode(node));
         // Add aliases for some common names that map to scientific names in the tree.
         _.forEach(commonToSciNames, function(sciName, commonName) {
             var label, addMyLabel;
             if (searchObj[sciName]) {
                 label = sciName + ' (' + commonName + ')';
                 addMyLabel = addCategory.bind(null, label);
                 searchObj[commonName] = _.map(searchObj[sciName], addMyLabel);
             }
         });
     }
 
     function makeStripe(id, color, stripeHeight, scrollTop, onClickStripe) {
         // Return an empty div with specified background color and height
         var $stripe = $('<div class="jwRainbowStripe">');
         $stripe.attr('id', 'rainbowStripe' + id);
         $stripe.attr('title', 'Click to scroll the tree display');
         $stripe.css('background-color', color);
         $stripe.height(stripeHeight);
         $stripe.click(onClickStripe.bind(null, scrollTop));
         return $stripe;
     }
 
     function makeRainbowSliderStripes($slider, onClickStripe, svgHeight, stripeColors, stripeTops) {
         // Set up the rainbow slider bar for the speciesPicker.
         // The stripeColors array determines the number of stripes and their colors.
         // stripeTops contains the pixel y coordinates of the top of each stripe;
         // we convert these to pixel heights of HTML div stripes.
         // onClickStripe is bound to the normalized top coord of each stripe
         // (the ratio of stripe's top coord to slider bar height, in the range
         // 0.0 to (1.0 - 1/stripeColors.length)).
         var i, $stripe, scrollTop, stripeHeight, svgStripeHeight;
         var sliderHeight = $('#speciesPicker').outerHeight();
         $slider.empty();
         if (stripeTops[0] > 0) {
             // Add a placeholder stripe for hubs at the top
             stripeHeight = sliderHeight * stripeTops[0] / svgHeight;
             $stripe = makeStripe('Hub', rainbow.hubColor, stripeHeight, 0, onClickStripe);
             $slider.append($stripe);
         }
         for (i = 0;  i < stripeColors.length;  i++) {
             svgStripeHeight = stripeTops[i+1] - stripeTops[i];
             stripeHeight = sliderHeight * svgStripeHeight / svgHeight;
             scrollTop = stripeTops[i] / svgHeight;
             $stripe = makeStripe(i, stripeColors[i], stripeHeight, scrollTop, onClickStripe);
             $slider.append($stripe);
         }
     }
 
     function resizeSliderIcon($sliderIcon, svgHeight, sliderBarHeight) {
         // Make the slider icon's height cover the same portion of the slider bar
         // as the visible part of the tree is compared to the entire tree image height.
         var visiblePortion = _.min([1, sliderBarHeight / svgHeight]);
         var iconHeight = visiblePortion * sliderBarHeight;
         // Set the icon rectangle's height and triangle vertical offset.
         var svg = document.getElementById('sliderSvg');
         var rect = document.getElementById('sliderRectangle');
         var tri = document.getElementById('sliderTriangle');
         var strokeWidth = 3, triangleHeight = 6;
         svg.setAttribute('height', iconHeight);
         rect.setAttribute('height', iconHeight - strokeWidth);
         tri.setAttribute('d', 'm 2.5,' + ((iconHeight - triangleHeight) / 2) + ' 0,6 4,-3 z');
         $sliderIcon.height(iconHeight);
     }
 
     function initRainbowSlider(svgHeight, stripeColors, stripeTops) {
         // Once we know the height of the hubs & tree image, initialize the rainbow slider
         // widget for coordinated scrolling.  Dragging the slider causes the image to scroll.
         // Scrolling the image causes the slider to move.  Clicking on a stripe causes the
         // image to scroll and the slider to move.
         var $speciesPicker = $('#speciesPicker');
         var $sliderBar = $('#rainbowSlider');
         var sliderBarTop = $sliderBar.offset().top;
         var sliderBarHeight = $speciesPicker.outerHeight();
         var $sliderIcon = $('#sliderSvgContainer');
         var sliderIconLeft = $sliderIcon.offset().left;
         var $speciesTree = $('#speciesTree');
         // When the user moves the slider, causing the image to scroll, don't do the
         // image onscroll action (do that only when the user scrolls the image).
         var inhibitImageOnScroll = false;
         // Don't let the slider hang off the bottom when the user clicks the bottom stripe:
         var maxNormalizedTop = 1 - (sliderBarHeight / svgHeight);
 
         // Define several helper functions within this function scope so they can use
         // the variables defined above.
         var scrollImage = function(normalizedTop) {
             // Scroll the hubs+tree image to a normalized top coord scaled by svgHeight.
             $speciesPicker.scrollTop(svgHeight * normalizedTop);
         };
 
         var moveSlider = function(normalizedTop) {
             // Move the slider icon to a normalized top coord scaled by sliderBarHeight.
             $sliderIcon.offset({ top: sliderBarTop + (normalizedTop * sliderBarHeight),
                                  left: sliderIconLeft.left });
         };
 
         var onClickStripe = function(normalizedTop) {
             // The user clicked a stripe; move the slider to the top of that stripe and
             // scroll the tree image to the top of the corresponding stripe in the image.
             inhibitImageOnScroll = true;
             if (normalizedTop > maxNormalizedTop) {
                 normalizedTop = maxNormalizedTop;
             }
             scrollImage(normalizedTop);
             moveSlider(normalizedTop);
         };
 
         var onDragSlider = function(event, ui) {
             // The user dragged the slider; scroll the tree image to the corresponding
             // position.
             var sliderTop = ui.offset.top - sliderBarTop;
             var normalizedTop = sliderTop / sliderBarHeight;
             inhibitImageOnScroll = true;
             scrollImage(normalizedTop);
         };
 
         var onScrollImage = function() {
             // The user scrolled the image -- or the user did something else which caused
             // the image to scroll, in which case we don't need to do anything more.
             var imageTop, normalizedTop;
             if (inhibitImageOnScroll) {
                 inhibitImageOnScroll = false;
                 return;
             }
             imageTop = -$speciesTree.offset().top + sliderBarTop + 1;
             normalizedTop = imageTop / svgHeight;
             moveSlider(normalizedTop);
         };
 
         // This might be called before the species image has been created; if so, do nothing.
         if (! $speciesTree || speciesTree.length === 0) {
             return;
         }
 
         makeRainbowSliderStripes($sliderBar, onClickStripe, svgHeight, stripeColors, stripeTops);
         resizeSliderIcon($sliderIcon, svgHeight, sliderBarHeight);
         $sliderIcon.draggable({ axis: 'y',
                                 containment: '#speciesGraphic',
                                 drag: onDragSlider
                                 });
         $speciesPicker.scroll(onScrollImage);
     }
 
     function findScrollbarWidth() {
         var widthPlain = $("#sbTestContainerDPlain").width();
         var widthInsideScroll = $("#sbTestContainerDInsideScroll").width();
         $('#sbTestContainer').hide();
         return widthPlain - widthInsideScroll;
     }
 
+    function updateGoButtonPosition() {
+        // If there's enough room for the Go button to be to the right of the inputs,
+        // set its height to the midpoint of theirs.
+        var $goButton = $('.jwGoButtonContainer');
+        var goOffset = $goButton.offset();
+        var menuOffset = $('#selectAssembly').offset();
+        var inputOffset = $('#positionInput').offset();
+        var verticalMidpoint = (menuOffset.top + inputOffset.top) / 2;
+        if (goOffset.left > inputOffset.left) {
+            $goButton.offset({top: verticalMidpoint });
+        } else {
+            // If the window shrinks and there's no longer room for the button, undo the above.
+            $goButton.css('top', 0);
+        }
+    }
+
     function setRightColumnWidth() {
         // Adjust the width of the "Find Position" section so it fits to the right of the
         // "Browse/Select Species" section.
         var ieFudge = scrollbarWidth ? scrollbarWidth + 4 : 0;
         var extraFudge = 4;
         var rightColumnWidth = ($('#pageContent').width() -
                                 $('#selectSpeciesSection').width() -
                                 ieFudge - extraFudge);
         if (rightColumnWidth >= 400) {
             $('#findPositionSection').width(rightColumnWidth);
         }
+        updateGoButtonPosition();
     }
 
     function setSpeciesPickerSizes(svgWidth, svgHeight) {
         // Adjust widths and heights of elements in #speciesPicker according to svg size.
         $('#speciesTree').width(svgWidth);
         $('#speciesTree').height(svgHeight);
         $('#speciesTreeContainer').height(svgHeight);
         // Make #speciesTreeContainer skinnier if a scrollbar is taking up space
         // in #speciesPicker.
         var leftover = ($("#speciesPicker").width() - scrollbarWidth);
         $("#speciesTreeContainer").width(leftover);
     }
 
     function highlightLabel(selectedName, scrollToItem) {
         // Highlight the selected species.
         // jQuery (at least our old version of it) can find the SVG text elements but can't
         // directly manipulate their class, so do it manually.
         var $sp = $('#speciesPicker');
         var y = $sp.scrollTop();
         $('svg text').each(function(ix, el) {
             var elName = el.getAttribute('name');
             var elClass = el.getAttribute('class');
             if (!elClass || elClass.indexOf('trackHubsLabel') < 0) {
                 if (elName === selectedName) {
                     el.setAttribute('class', 'selected');
                     y = el.getAttribute('y');
                 } else if (elClass === 'selected') {
                     el.setAttribute('class', '');
                 }
             }
         });
         if (scrollToItem) {
             $sp.scrollTop(y - 100);
         }
     }
 
     function hubNameFromDb(db) {
         var matches = db ? db.match(/^(hub_[0-9]+)_/) : null;
         if (matches) {
             return matches[1];
         } else {
             return null;
         }
     }
 
     function highlightLabelForDb(db, taxId) {
         var hubName = hubNameFromDb(db);
         if (hubName) {
             highlightLabel('textEl_' + hubName, true);
         } else {
             highlightLabel('textEl_' + taxId, true);
         }
     }
 
     function pruneInactive(node, activeGenomes, activeTaxIds) {
         // Return true if some leaf descendant of node is in activeGenomes or activeTaxIds.
         // Remove any child that returns false.
         // If one of {genome, taxId} matches but not the other, tweak the other to match dbDb,
         // Since we'll be using the hgwdev dbDbTree on the RR which may have been tweaked.
         var genome = node[0], taxId = node[1], kids = node[3];
         var hasActiveLeaf = false, i, dbDbTaxId, dbDbGenome;
         if (!kids || kids.length === 0) {
             // leaf node: is it active?
             dbDbTaxId = activeGenomes[genome];
             if (dbDbTaxId) {
                 hasActiveLeaf = true;
                 node[1] = dbDbTaxId;
             }
             // Yet another special case for Baboon having one genome with two species...
             // maybe we should just change dbDb?
             else if (_.startsWith(genome, 'Baboon ') && (taxId === 9555 || taxId === 9562)) {
                 hasActiveLeaf = true;
             } else {
                 dbDbGenome = activeTaxIds[taxId];
                 if (dbDbGenome) {
                     hasActiveLeaf = true;
                     node[0] = dbDbGenome;
                 }
             }
         } else {
             // parent node: splice out any child nodes with no active leaves
             for (i = kids.length - 1;  i >= 0;  i--) {
                 if (pruneInactive(kids[i], activeGenomes, activeTaxIds)) {
                     hasActiveLeaf = true;
                 } else {
                     kids.splice(i, 1);
                 }
             }
         }
         return hasActiveLeaf;
     }
 
-    function drawSpeciesPicker() {
+    function drawSpeciesPicker(dbDbTree) {
         // If dbDbTree is nonempty and SVG is supported, draw the tree; if SVG is not supported,
         // use the space to suggest that the user install a better browser.
         // If dbDbTree doesn't exist, leave the "Represented Species" section hidden.
         var svg, spTree, stripeTops;
         if (dbDbTree) {
             if (document.createElementNS) {
                 // Draw the phylogenetic tree and do layout adjustments
                 svg = document.getElementById('speciesTree');
                 spTree = speciesTree.draw(svg, dbDbTree, uiState.hubs,
                                           { onClickSpeciesName: 'hgGateway.onClickSpeciesLabel',
                                             onClickHubName: 'hgGateway.onClickHubName',
                                             hgHubConnectUrl: 'hgHubConnect?hgsid=' + window.hgsid,
                                             containerWidth: $('#speciesPicker').width()
                                             });
                 setSpeciesPickerSizes(spTree.width, spTree.height);
                 highlightLabelForDb(uiState.db, uiState.taxId);
                 stripeTops = rainbow.draw(svg, dbDbTree,
                                           spTree.yTree, spTree.height, spTree.leafTops);
             } else {
                 $('#speciesTreeContainer').html(getBetterBrowserMessage);
             }
             $('#representedSpeciesTitle').show();
             $('#speciesGraphic').show();
-            if (dbDbTree) {
+            if (dbDbTree && document.createElementNS) {
                 // This needs to be done after things are visible so the slider gets the
                 // right position.
                 initRainbowSlider(spTree.height, rainbow.colors, stripeTops);
             }
         }
     }
 
     function onSelectGene(item) {
         // Set the position from an autocomplete result;
         // set hgFindMatches and make sure suggestTrack is in pack mode for highlighting the match.
         var newPos = item.id;
         var settings;
         $('#positionDisplay').text(newPos);
         if (uiState.suggestTrack) {
             settings = { 'hgFind.matches': item.internalId };
             settings[uiState.suggestTrack] = 'pack';
             cart.send({ cgiVar: settings });
             cart.flush();
         }
         // Overwrite the selected item w/actual position after the autocomplete plugin is done:
         function overwriteWithPos() {
             $('#positionInput').val(newPos);
         }
         window.setTimeout(overwriteWithPos, 0);
     }
 
-    function updateGoButtonPosition() {
-        // If there's room for the go button to appear to the right of #selectAssembly and
-        // #positionInput, align the top of the go button with the bottom of #selectAssembly.
-        // Otherwise let it hang out below.
-        var $fpic = $('#findPosInputContainer');
-        var fpicOffset = $fpic.offset();
-        var fpicRight = fpicOffset.left + $fpic.width();
-        var $button = $('.jwGoButtonContainer');
-        var buttonOffset = $button.offset();
-        var $select;
-        if (buttonOffset.left > fpicRight) {
-            // Align button top with select bottom.
-            $select = $('#selectAssembly');
-            buttonOffset.top = $select.offset().top + $select.height();
-        } else {
-            // Button wraps around to below inputs; remove any previous vertical offsetting.
-            buttonOffset.top = fpicOffset.top + $fpic.height() + 10;
-        }
-        $button.offset(buttonOffset);
-    }
-
     function setAssemblyOptions(uiState) {
         var assemblySelectLabel = 'Assembly';
         if (uiState.dbOptions) {
             var html = '', option, i, selected;
             for (i = 0;  i < uiState.dbOptions.length;  i++) {
                 option = uiState.dbOptions[i];
                 selected = (option.value === uiState.db) ? 'selected ' : '';
                 html += '<option ' + selected + 'value="' + option.value + '">' +
                         option.label + '</option>';
             }
             $('#selectAssembly').html(html);
         }
         if (uiState.genomeLabel) {
             if (uiState.hubUrl && uiState.genomeLabel.indexOf('Hub') < 0) {
                 assemblySelectLabel = uiState.genomeLabel + ' Hub Assembly';
             } else {
                 assemblySelectLabel = uiState.genomeLabel + ' Assembly';
             }
         }
         $('#selectAssemblyLabel').text(assemblySelectLabel);
     }
 
     function trackHubSkipHubName(name) {
         // Just like hg/lib/trackHub.c's...
         var matches;
         if (name && (matches = name.match(/^hub_[0-9]+_(.*)/)) !== null) {
             return matches[1];
         } else {
             return name;
         }
     }
 
     function setAssemblyDescriptionTitle(db, genome) {
         $('#descriptionGenome').html(trackHubSkipHubName(genome));
         $('#descriptionDb').html(trackHubSkipHubName(db));
     }
 
     function tweakDescriptionPhotoWidth() {
         // Our description.html files assume a pretty wide display area, but now we're
         // squeezed to the right of the 'Select Species' section.  If there's a large
         // image, scale it down.  The enclosing table is usually sized to leave a lot
         // of space to the left of the image, so shrink that too.
         // This must be called *after* #descriptionText is updated with the new content.
         var width, scaleFactor, newWidth;
         var $table = $('#descriptionText table').first();
         var $img = $('#descriptionText table img').first();
         if ($img.length) {
             width = $img.width();
             if (width > 175) {
                 // Scale to 150px wide, preserving aspect ratio
                 newWidth = 150;
                 scaleFactor = newWidth / width;
                 $img.width(newWidth)
                     .height($img.height() * scaleFactor);
                 width = newWidth;
             }
             if ($table.width() - width > 20) {
                 $table.width(width + 10);
             }
             // hg19's description.html sets a height for its table that pushes the
             // links section down; unneeded & unwanted here, so remove height if set:
             $table.removeAttr('height');
         }
     }
 
     function updateDescription(description) {
         // We got the contents of a db's description.html -- tweak its format to fit
         // the new design.
         $('#descriptionText').html(description);
         tweakDescriptionPhotoWidth();
         // Apply JWest formatting to all anchors in description.
         // We can't simply style all <a> tags that way because autocomplete uses <a>'s.
         $('#descriptionText a').addClass('jwAnchor');
         // Apply square bullet style to all ul's in description.
         $('#descriptionText ul').addClass('jwNoBullet');
         $('#descriptionText li').addClass('jwSquareBullet');
     }
 
+    function initFindPositionContents() {
+        // Unhide contents of Find Position section and adjust layout.
+        $('#findPositionContents').show();
+        // Set assembly menu's width to same as position input.
+        var posWidth = $('#positionInput').outerWidth();
+        var $select = $('#selectAssembly');
+        $select.outerWidth(posWidth);
+        // For some reason, it doesn't set it to posWidth, it sets it to posWidth-2...
+        // detect and adjust.
+        var weirdDiff = posWidth - $select.outerWidth();
+        if (weirdDiff) {
+            $select.outerWidth(posWidth + weirdDiff);
+        }
+        updateGoButtonPosition();
+    }
+
     function updateFindPositionSection(uiState) {
+        // Update the assembly menu, positionInput and description.
         var suggestUrl = null;
         if (uiState.suggestTrack) {
             suggestUrl = 'hgSuggest?db=' + uiState.db + '&prefix=';
         }
         setAssemblyOptions(uiState);
         if (uiState.position) {
             $('#positionDisplay').text(uiState.position);
         }
         autocompleteCat.init($('#positionInput'),
                              { baseUrl: suggestUrl,
                                watermark: positionWatermark,
                                onSelect: onSelectGene,
                                enterSelectsIdentical: true,
                                onEnterTerm: goToHgTracks });
         setAssemblyDescriptionTitle(uiState.db, uiState.genome);
         updateDescription(uiState.description);
-        if (uiState.db) {
-            $('#findPositionContents').show();
+        if (uiState.db && $('#findPositionContents').css('display') === 'none') {
+            initFindPositionContents();
         }
-        updateGoButtonPosition();
     }
 
     function removeDups(inList, isDup) {
         // Return a list with only unique items from inList, using isDup(a, b) -> true if a =~ b
         var inLength = inList.length;
         // inListDups is an array of boolean flags for marking duplicates, parallel to inList.
         var inListDups = [];
         var outList = [];
         var i, j;
         for (i = 0;  i < inLength;  i++) {
             // If something has already been marked as a duplicate, skip it.
             if (! inListDups[i]) {
                 // the first time we see a value, add it to outList.
                 outList.push(inList[i]);
                 for (j = i+1;  j < inLength;  j++) {
                     // Now scan the rest of inList to find duplicates of inList[i].
                     // We can skip items previously marked as duplicates.
                     if (!inListDups[j] && isDup(inList[i], inList[j])) {
                         inListDups[j] = true;
                     }
                 }
             }
         }
         return outList;
     }
 
     function speciesResultsEquiv(a, b) {
         // For autocompleteCat's option isDuplicate: return true if species search results
         // a and b would be redundant (and hence one should be removed).
         if (a.db !== b.db) {
             return false;
         } else if (a.genome === b.genome) {
             return true;
         }
         return false;
     }
 
     function searchByKeyNoCase(searchObj, term) {
         // Return a concatenation of searchObj list values whose keys start with term
         // (case-insensitive).
         var termUpCase = term.toUpperCase();
         var searchObjResults = [];
         _.forEach(searchObj, function(results, key) {
             if (_.startsWith(key.toUpperCase(), termUpCase)) {
                 searchObjResults = searchObjResults.concat(results);
             }
         });
         return searchObjResults;
     }
 
     function processSpeciesAutocompleteItems(searchObj, results, term) {
         // This (bound to searchObj) is passed into autocompleteCat as options.onServerReply.
         // The server sends a list of items that may include duplicates and can have
         // results from dbDb and/or assembly hubs.  Also look for results from the
         // phylogenetic tree, and insert those before the assembly hub matches.
         // Then remove duplicates and return the processed results which will then
         // be used to render the menu.
         var phyloResults = searchByKeyNoCase(searchObj, term);
         var hubResultIx = _.findIndex(results, function(result) { return !! result.hubUrl; });
         var hubResults = hubResultIx >= 0 ? results.splice(hubResultIx) : [];
         var combinedResults = results.concat(phyloResults).concat(hubResults);
         return removeDups(combinedResults, speciesResultsEquiv);
     }
 
     // Server response event handlers
 
     function checkJsonData(jsonData, callerName) {
         // Return true if jsonData isn't empty and doesn't contain an error;
         // otherwise complain on behalf of caller.
         if (! jsonData) {
             alert(callerName + ': empty response from server');
         } else if (jsonData.error) {
             console.error(jsonData.error);
             alert(callerName + ': error from server: ' + jsonData.error);
         } else {
             if (debugCartJson) {
                 console.log('from server:\n', jsonData);
             }
             return true;
         }
         return false;
     }
 
     function updateStateAndPage(jsonData) {
         // Update uiState with new values and update the page.
         var hubsChanged = !_.isEqual(jsonData.hubs, uiState.hubs);
         // In rare cases, there can be a genome (e.g. Baboon) with multiple species/taxIds
         // (e.g. Papio anubis for papAnu1 vs. Papio hamadryas for papHam1).  Changing the
         // db can result in changing the taxId too.  In that case, update the highlighted
         // species in the tree image.
         if (jsonData.taxId !== uiState.taxId) {
             highlightLabel('textEl_' + jsonData.taxId, false);
         }
         _.assign(uiState, jsonData);
         updateFindPositionSection(uiState);
         if (hubsChanged) {
-            drawSpeciesPicker();
+            drawSpeciesPicker(prunedDbDbTree);
         }
     }
 
     function handleRefreshState(jsonData) {
         if (checkJsonData(jsonData, 'handleRefreshState')) {
             updateStateAndPage(jsonData);
         }
     }
 
     function handleSetDb(jsonData) {
         // Handle the server's response to cartJson command setDb or setHubDb
         if (checkJsonData(jsonData, 'handleSetDb') &&
             trackHubSkipHubName(jsonData.db) === trackHubSkipHubName(uiState.db)) {
             updateStateAndPage(jsonData);
         } else {
             console.log('handleSetDb ignoring: ' + trackHubSkipHubName(jsonData.db) +
                         ' !== ' + trackHubSkipHubName(uiState.db));
         }
     }
 
     function handleSetTaxId(jsonData) {
         // Handle the server's response to the setTaxId cartJson command.
         if (checkJsonData(jsonData, 'handleSetTaxId') && jsonData.taxId === uiState.taxId) {
             // Update uiState with new values and update the page:
             _.assign(uiState, jsonData);
             updateFindPositionSection(uiState);
         } else {
             console.log('handleSetTaxId ignoring: ' + jsonData.taxId +
                         ' !== ' + uiState.taxId);
         }
     }
 
     // UI Event Handlers
 
     function clearWatermarkInput($input, watermark) {
         // Note: it is not necessary to re-.Watermark if we upgrade the plugin to version >= 3.1
         $input.val('').Watermark(watermark);
     }
 
     function clearSpeciesInput() {
         // Replace anything typed into the species input with the watermark.
         clearWatermarkInput($('#speciesSearch'), speciesWatermark);
     }
 
     function clearPositionInput() {
         // Replace anything typed into the position input with the watermark.
         clearWatermarkInput($('#positionInput'), positionWatermark);
     }
 
     function setTaxId(taxId, db, doScrollToItem, doClearSpeciesInput) {
         // The user has selected a species (and possibly even a particular database) --
         // if we're not already using it, change to it.
         var cmd;
         if (taxId !== uiState.taxId || (db && db !== uiState.db)) {
             uiState.taxId = taxId;
             uiState.hubUrl = null;
             cmd = { setTaxId: { taxId: '' + taxId } };
             if (db) {
                 uiState.db = db;
                 cmd.setTaxId.db = db;
             }
             cart.send(cmd, handleSetTaxId);
             cart.flush();
             clearPositionInput();
         }
         highlightLabel('textEl_' + taxId, doScrollToItem);
         if (doClearSpeciesInput) {
             clearSpeciesInput();
         }
   }
 
     function setHubDb(hubUrl, taxId, db, hubName, isAutocomplete) {
         // User clicked on a hub name (switch to its default genome) or selected an
         // assembly hub from autocomplete (switch to that assembly hub db).
         var cmd;
         if (hubUrl !== uiState.hubUrl ||
             (isAutocomplete && db !== uiState.db)) {
             uiState.hubUrl = hubUrl;
             uiState.taxId = taxId;
             uiState.db = trackHubSkipHubName(db);
             // Use cart variables to connect to the selected hub and switch to db
             // (hubConnectLoadHubs, called by cartNew)
             cmd = { cgiVar: { hubUrl: hubUrl,
                               genome: trackHubSkipHubName(db) },
                     setHubDb: { hubUrl: hubUrl,
                                 taxId: '' + taxId }
                     };
             cart.send(cmd, handleSetDb);
             cart.flush();
             clearPositionInput();
         }
         highlightLabel('textEl_' + hubName, isAutocomplete);
         if (! isAutocomplete) {
             clearSpeciesInput();
         }
     }
 
 
     function setDbFromAutocomplete(item) {
         // The user has selected a result from the species-search autocomplete.
         // It might be a taxId and/or db from dbDb, or it might be a hub db.
         var taxId = item.taxId || -1;
         var db = item.db;
         if (item.hubUrl) {
             // The autocomplete sends the hub database from hubPublic.dbList,
             // without the hub prefix -- restore the prefix here.
             db = item.hubName + '_' + item.db;
             setHubDb(item.hubUrl, taxId, db, item.hubName, true);
         } else {
             setTaxId(taxId, item.db, true, false);
         }
     }
 
     function onClickSpeciesLabel(taxId) {
         // When user clicks on a label, use that taxId (default db);
         // don't scroll to the label because if they clicked on it they can see it already;
         // do clear the autocomplete input.
         setTaxId(taxId, null, false, true);
     }
 
     function onClickHubName(hubUrl, taxId, db, hubName) {
         // This is just a wrapper -- the draw module has to know all about the contents
         // of each hub object in hubList anyway.
         setHubDb(hubUrl, taxId, db, hubName, false);
     }
 
     function onChangeDbMenu() {
         // The user selected a different db for this genome; get db info from server.
         var db = $('#selectAssembly').val();
         var cmd;
         if (db !== uiState.db) {
             setAssemblyDescriptionTitle(db, uiState.genome);
             cmd = { setDb: { db: db } };
             if (uiState.hubUrl) {
                 cmd.setDb.hubUrl = uiState.hubUrl;
             }
             cart.send(cmd, handleSetDb);
             cart.flush();
             uiState.db = db;
             clearPositionInput();
         }
     }
 
     function onClickCopyPosition() {
         // Copy the displayed position into the position input:
         var posDisplay = $('#positionDisplay').text();
         $('#positionInput').val(posDisplay);
     }
 
     function goToHgTracks() {
         // Create and submit a form for hgTracks with hidden inputs for org, db and position.
         var position = $('#positionInput').val();
         var posDisplay = $('#positionDisplay').text();
         var pix = uiState.pix || calculateHgTracksWidth();
         var $form;
         if (! position || position === '' || position === positionWatermark) {
             position = posDisplay;
         }
         // Show a spinner -- sometimes it takes a while for hgTracks to start displaying.
         $('.jwGoIcon').removeClass('fa-play').addClass('fa-spinner fa-spin');
         // Make a form and submit it.  In order for this to work in IE, the form
         // must be appended to the body.
         $form = $('<form action="hgTracks" method=GET id="mainForm">' +
                   '<input type=hidden name="org" value="' + uiState.genome + '">' +
                   '<input type=hidden name="db" value="' + uiState.db + '">' +
                   '<input type=hidden name="position" value="' + position + '">' +
                   '<input type=hidden name="pix" value="' + pix + '">' +
                   '</form>');
         $('body').append($form);
         $form.submit();
     }
 
     function replaceHgsidInLinks() {
         // Substitute '$hgsid' with real hgsid in <a> href's.
         $('a').each(function(ix, aEl) {
             var href = aEl.getAttribute('href');
             if (href && href.indexOf('$hgsid') >= 0) {
                 aEl.setAttribute('href', href.replace('$hgsid', window.hgsid));
             }
         });
     }
 
     function init() {
         // Boot up the page; initialize elements and install event handlers.
         var searchObj = {};
         // We need a bound function to pass into autocompleteCat.init below;
         // however, autocompleteFromTree is even slower than drawing the tree because of
         // all the copying.  So bind now, fill in searchObj later.
         var processSpeciesResults = processSpeciesAutocompleteItems.bind(null, searchObj);
         cart.setCgi('hgGateway');
         cart.debug(debugCartJson);
         // Get state from cart
         cart.send({ getUiState: {} }, handleRefreshState);
         cart.flush();
         // Prune inactive genomes from dbDbTree.
         var activeTaxIds = _.invert(activeGenomes);
+        prunedDbDbTree = dbDbTree;
         if (dbDbTree && ! pruneInactive(dbDbTree, activeGenomes, activeTaxIds)) {
-            // no more dbDbTree descendants left after pruning
-            dbDbTree = null;
+            prunedDbDbTree = null;
         }
 
         // When page has loaded, do layout adjustments and initialize event handlers.
         $(function() {
             scrollbarWidth = findScrollbarWidth();
             setRightColumnWidth();
             setupFavIcons();
             autocompleteCat.init($('#speciesSearch'),
                                  { baseUrl: 'hgGateway?hggw_term=',
                                    watermark: speciesWatermark,
                                    onSelect: setDbFromAutocomplete,
                                    onServerReply: processSpeciesResults,
                                    enterSelectsIdentical: true });
             $('#selectAssembly').change(onChangeDbMenu);
             $('#positionDisplay').click(onClickCopyPosition);
             $('#copyPosition').click(onClickCopyPosition);
             $('.jwGoButtonContainer').click(goToHgTracks);
             $(window).resize(setRightColumnWidth.bind(null, scrollbarWidth));
-            $(window).resize(updateGoButtonPosition);
             replaceHgsidInLinks();
             // Fill in searchObj here once everything is displayed.
-            autocompleteFromTree(dbDbTree, searchObj);
+            autocompleteFromTree(prunedDbDbTree, searchObj);
         });
     }
 
     return { init: init,
              // For use by speciesTree.draw SVG (text-only onclick):
              onClickSpeciesLabel: onClickSpeciesLabel,
              onClickHubName: onClickHubName
            };
 
 }()); // hgGateway
 
 hgGateway.init();