7e2b9931df4716b1d3bb64732fc4732412a113c1
angie
  Wed Apr 13 13:14:14 2016 -0700
Added support for removal of duplicate items from species autocomplete results.
refs #15277 (see note 142)

diff --git src/hg/js/hgGateway.js src/hg/js/hgGateway.js
index 76a6401..1b93dae 100644
--- src/hg/js/hgGateway.js
+++ src/hg/js/hgGateway.js
@@ -636,54 +636,91 @@
                               }
                               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.
+        // 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);
+                }
                 cache[term] = combinedResults;
                 acCallback(combinedResults);
             });
             // 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);
@@ -1275,30 +1312,41 @@
         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 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;
+    }
+
     // 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;
@@ -1512,30 +1560,31 @@
 
         // When page has loaded, draw the species tree, do layout adjustments and
         // initialize event handlers.
         $(function() {
             var searchObj = autocompleteFromTree(dbDbTree);
             addAutocompleteCommonNames(searchObj);
             scrollbarWidth = findScrollbarWidth();
             drawSpeciesPicker();
             setRightColumnWidth();
             setupFavIcons();
             autocompleteCat.init($('#speciesSearch'),
                                  { baseUrl: 'hgGateway?hggw_term=',
                                    watermark: speciesWatermark,
                                    onSelect: setDbFromAutocomplete,
                                    searchObj: searchObj,
+                                   isDuplicate: speciesResultsEquiv,
                                    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,