68fcf0d16d6408ac16200557b687224bd86277e0
angie
  Wed Apr 13 16:24:49 2016 -0700
Refactored autocompleteCat options so that species autocomplete can do even more customized post-processing of results from server (insert phylo tree matches between dbDb matches and assembly hub matches).  refs #15277

diff --git src/hg/js/hgGateway.js src/hg/js/hgGateway.js
index 1b93dae..2fde271 100644
--- src/hg/js/hgGateway.js
+++ src/hg/js/hgGateway.js
@@ -636,121 +636,85 @@
                               }
                               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 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 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.
-        // If options.searchObj is provided, it is used in addition to baseUrl; first the term is
-        // looked up in searchObj and then also queried using baseUrl.  Values in searchObj
-        // should have the same structure as the value returned by a baseUrl query.
-        // options.isDuplicate (if provided) is a function (a, b) -> boolean that returns
-        // true if autocomplete items a and b are redundant; it is used to remove duplicates
-        // from autocomplete results.
+        // 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;
-            var searchObjResults = [];
-            _.forEach(options.searchObj, function(results, key) {
-                if (_.startsWith(key.toUpperCase(), term.toUpperCase())) {
-                    searchObjResults = searchObjResults.concat(results);
-                }
-            });
             $.getJSON(url)
                .done(function(results) {
-                var combinedResults = results.concat(searchObjResults);
-                // Optionally remove duplicates identified by options.isDuplicate
-                if (options.isDuplicate) {
-                    combinedResults = removeDups(combinedResults, options.isDuplicate);
+                if (_.isFunction(options.onServerReply)) {
+                    results = options.onServerReply(results, term);
                 }
-                cache[term] = combinedResults;
-                acCallback(combinedResults);
+                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.searchObj = options.searchObj || {};
         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');
         }
     }
@@ -860,44 +824,40 @@
                     var kidObj = autocompleteFromTree(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 addAutocompleteCommonNames(searchObj) {
-        // After searchObj is constructed by autocompleteFromTree, add aliases for
-        // some common names that map to scientific names in the tree.
+        // 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);
             }
         });
+        return searchObj;
     }
 
     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.
@@ -1312,41 +1272,92 @@
         setAssemblyOptions(uiState);
         if (uiState.position) {
             $('#positionDisplay').text(uiState.position);
         }
         autocompleteCat.init($('#positionInput'),
                              { baseUrl: suggestUrl,
                                watermark: positionWatermark,
                                onSelect: onSelectGene,
                                enterSelectsIdentical: true,
                                onEnterTerm: goToHgTracks });
         updateGoButtonPosition();
         setAssemblyDescriptionTitle(uiState.db, uiState.genome);
         updateDescription(uiState.description);
     }
 
+    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;
@@ -1550,41 +1561,40 @@
         });
     }
 
     function init() {
         // Boot up the page; initialize elements and install event handlers.
         cart.setCgi('hgGateway');
         cart.debug(debugCartJson);
         // Get state from cart
         cart.send({ getUiState: {} }, handleRefreshState);
         cart.flush();
 
         // When page has loaded, draw the species tree, do layout adjustments and
         // initialize event handlers.
         $(function() {
             var searchObj = autocompleteFromTree(dbDbTree);
-            addAutocompleteCommonNames(searchObj);
+            var processSpeciesResults = processSpeciesAutocompleteItems.bind(null, searchObj);
             scrollbarWidth = findScrollbarWidth();
             drawSpeciesPicker();
             setRightColumnWidth();
             setupFavIcons();
             autocompleteCat.init($('#speciesSearch'),
                                  { baseUrl: 'hgGateway?hggw_term=',
                                    watermark: speciesWatermark,
                                    onSelect: setDbFromAutocomplete,
-                                   searchObj: searchObj,
-                                   isDuplicate: speciesResultsEquiv,
+                                   onServerReply: processSpeciesResults,
                                    enterSelectsIdentical: true });
             updateFindPositionSection(uiState);
             $('#selectAssembly').change(onChangeDbMenu);
             $('#positionDisplay').click(onClickCopyPosition);
             $('#copyPosition').click(onClickCopyPosition);
             $('.jwGoButtonContainer').click(goToHgTracks);
             $(window).resize(setRightColumnWidth.bind(null, scrollbarWidth));
             $(window).resize(updateGoButtonPosition);
             replaceHgsidInLinks();
         });
     }
 
     return { init: init,
              // For use by speciesTree.draw SVG (text-only onclick):
              onClickSpeciesLabel: onClickSpeciesLabel,