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,