0d3b8feb6074f6caccc2d4aadd020961fbd7eb82
chmalee
  Wed Jun 10 14:39:38 2020 -0700
Add drag reorder of VCF samples to VCF trio display. Also fixes bug in haplotype sort that I didn't discover until adding drag reorder. Sometimes when sorting haplotypes it is the case that the initial best match to a child allele is the same for each parent. Previously I would just advance the first drawn parent to the next best match but now I also check whether advancing the first drawn is actually the best idea and potentially advance the other parent instead, refs #25582

diff --git src/hg/js/utils.js src/hg/js/utils.js
index 1e476f7..ad25c78 100644
--- src/hg/js/utils.js
+++ src/hg/js/utils.js
@@ -794,30 +794,39 @@
     $(inp).filter('[name]:enabled').each(function (i) {
         var name  = $(this).attr('name');
         var val = $(this).val();
         if ($(this).attr('type') === 'checkbox') {
             name = cgiBooleanShadowPrefix() + name;
             val = $(this).attr('checked') ? 1 : 0;
         } else if ($(this).attr('type') === 'radio') {
             if (!$(this).attr('checked')) {
                 name = undefined;
             }
         }
         if (name && name !== "Submit" && val !== undefined && val !== null) {
             urlData[name] = val;
         }
     });
+    // special case the vcfSampleOrder variable because it is a hidden input type that
+    // changes based on click-drag
+    $(inp).filter('[name$="vcfSampleOrder"]').each(function (i) {
+        var name  = $(this).attr('name');
+        var val = $(this).val();
+        if (name && name !== "Submit" && val !== undefined && val !== null) {
+            urlData[name] = val;
+        }
+    });
     $(sel).filter('[name]:enabled').each(function (i) {
         var name  = $(this).attr('name');
         var val = $(this).val();
         if (name && val !== undefined && val !== null) {
             if (subtrackName && name === subtrackName) {
                 if (val === 'hide') {
                    urlData[name+"_sel"] = 0;    // Can't delete "_sel" because default takes over
                    urlData[name]        = "[]"; // Can delete vis because
                 } else {                        //     subtrack vis should be inherited.
                     urlData[name+"_sel"] = 1;
                     urlData[name]        = val;
                 }
             } else {
                 if ($.isArray( val) && val.length > 1) {
                     urlData[name] = "[" + val.toString() + "]";
@@ -3092,15 +3101,410 @@
 function escapeJQuerySelectorChars(str)
 {   // replace characters which are reserved in jQuery selectors
     // (surprisingly jQuery does not have a built in function to do this).
     return str.replace(/([!"#$%&'()*+,./:;<=>?@[\]^`{|}~"])/g,'\\$1');
 }
 
 var preloadImages = [];
 var preloadImageCount = 0;
 function preloadImg(url)  // DEAD CODE?
 {
 // force an image to be loaded (e.g. for images in menus or dialogs).
     preloadImages[preloadImageCount] = new Image();
     preloadImages[preloadImageCount].src = url;
     preloadImageCount++;
 }
+
+
+  ///////////////////
+ /////  mouse  /////
+///////////////////
+var mouse = {
+
+    savedOffset: {x:0, y:0},
+
+    saveOffset: function (ev)
+    {   // Save the mouse offset associated with this event
+        mouse.savedOffset = {x: ev.clientX, y: ev.clientY};
+    },
+
+    hasMoved: function (ev)
+    {   // return true if mouse has moved a significant amount
+        var minPixels = 10;
+        var movedX = ev.clientX - mouse.savedOffset.x;
+        var movedY = ev.clientY - mouse.savedOffset.y;
+        if (arguments.length === 2) {
+            var num = Number(arguments[1]);
+            if (isNaN(num)) {
+                if ( arguments[1].toLowerCase() === "x" )
+                    return (movedX > minPixels || movedX < (minPixels * -1));
+                if ( arguments[1].toLowerCase() === "y" )
+                    return (movedY > minPixels || movedY < (minPixels * -1));
+            }
+            else
+                minPixels = num;
+        }
+        return (   movedX > minPixels || movedX < (minPixels * -1)
+                || movedY > minPixels || movedY < (minPixels * -1));
+    }
+};
+
+  ///////////////////////////
+ //// Drag Reorder Code ////
+///////////////////////////
+var dragReorder = {
+
+    setOrder: function (table)
+    {   // Sets the 'order' value for the image table after a drag reorder
+        var varsToUpdate = {};
+        $("tr.imgOrd").each(function (i) {
+            if ($(this).attr('abbr') !== $(this).attr('rowIndex').toString()) {
+                $(this).attr('abbr',$(this).attr('rowIndex').toString());
+                var name = this.id.substring('tr_'.length) + '_imgOrd';
+                varsToUpdate[name] = $(this).attr('abbr');
+            }
+        });
+        if (objNotEmpty(varsToUpdate)) {
+            cart.setVarsObj(varsToUpdate);
+            imageV2.markAsDirtyPage();
+        }
+    },
+
+    sort: function (table)
+    {   // Sets the table row order to match the order of the abbr attribute.
+        // This is needed for back-button, and for visBox changes combined with refresh.
+        var tbody = $(table).find('tbody')[0];
+        if (!tbody)
+            tbody = table;
+
+        // Do we need to sort?
+        var trs = tbody.rows;
+        var needToSort = false;
+        $(trs).each(function(ix) {
+            if ($(this).attr('abbr') !== $(this).attr('rowIndex').toString()) {
+                needToSort = true;
+                return false;  // break for each() loops
+            }
+        });
+        if (!needToSort)
+            return false;
+
+        // Create array of tr holders to sort
+        var ary = [];
+        $(trs).each(function(ix) {  // using sortTable found in utils.js
+            ary.push(new sortTable.field(parseInt($(this).attr('abbr')),false,this));
+        });
+
+        // Sort the array
+        ary.sort(sortTable.fieldCmp);
+
+        // most efficient reload of sorted rows I have found
+        var sortedRows = jQuery.map(ary, function(ary, i) { return ary.row; });
+        $(tbody).append( sortedRows ); // removes tr from current position and adds to end.
+        return true;
+    },
+
+    showCenterLabel: function (tr, show)
+    {   // Will show or hide centerlabel as requested
+        // adjust button, sideLabel height, sideLabelOffset and centerlabel display
+
+        if (!$(tr).hasClass('clOpt'))
+            return;
+        var center = normed($(tr).find(".sliceDiv.cntrLab"));
+        if (!center)
+            return;
+        var seen = ($(center).css('display') !== 'none');
+        if (show === seen)
+            return;
+
+        var centerHeight = $(center).height();
+
+        var btn = normed($(tr).find("p.btn"));
+        var side = normed($(tr).find(".sliceDiv.sideLab"));
+        if (btn && side) {
+            var sideImg = normed($(side).find("img"));
+            if (sideImg) {
+                var top = parseInt($(sideImg).css('top'));
+                if (show) {
+                    $(btn).css('height',$(btn).height() + centerHeight);
+                    $(side).css('height',$(side).height() + centerHeight);
+                    top += centerHeight; // top is a negative number
+                    $(sideImg).css( {'top': top.toString() + "px" });
+                    $( center ).show();
+                } else if (!show) {
+                    $(btn).css('height',$(btn).height() - centerHeight);
+                    $(side).css('height',$(side).height() - centerHeight);
+                    top -= centerHeight; // top is a negative number
+                    $(sideImg).css( {'top': top.toString() + "px" });
+                    $( center ).hide();
+                }
+            }
+        }
+    },
+
+    getContiguousRowSet: function (row)
+    {   // Returns the set of rows that are of the same class and contiguous
+        if (!row)
+            return null;
+        var btn = $( row ).find("p.btn");
+        if (btn.length === 0)
+            return null;
+        var classList = $( btn ).attr("class").split(" ");
+        var matchClass = classList[0];
+        var table = $(row).parents('table#imgTbl')[0];
+        var rows = $(table).find('tr');
+
+        // Find start index
+        var startIndex = $(row).attr('rowIndex');
+        var endIndex = startIndex;
+        for (var ix=startIndex-1; ix >= 0; ix--) {
+            btn = $( rows[ix] ).find("p.btn");
+            if (btn.length === 0)
+                break;
+            classList = $( btn ).attr("class").split(" ");
+            if (classList[0] !== matchClass)
+                break;
+            startIndex = ix;
+        }
+
+        // Find end index
+        for (var rIx=endIndex; rIx<rows.length; rIx++) {
+            btn = $( rows[rIx] ).find("p.btn");
+            if (btn.length === 0)
+                break;
+            classList = $( btn ).attr("class").split(" ");
+            if (classList[0] !== matchClass)
+                break;
+            endIndex = rIx;
+        }
+        return rows.slice(startIndex,endIndex+1); // endIndex is 1 based!
+    },
+
+    getCompositeSet: function (row)
+    {   // Returns the set of rows that are of the same class and contiguous
+        if (!row)
+            return null;
+        var rowId = $(row).attr('id').substring('tr_'.length);
+        var rec = hgTracks.trackDb[rowId];
+        if (tdbIsSubtrack(rec) === false)
+            return null;
+
+        var rows = $('tr.trDraggable:has(p.' + rec.parentTrack+')');
+        return rows;
+    },
+
+    zipButtons: function (table)
+    {   // Goes through the image and binds composite track buttons when adjacent
+        var rows = $(table).find('tr');
+        var lastClass="";
+        var lastBtn = null;
+        var lastSide = null;
+        var lastMatchesLast=false;
+        var lastBlue=true;
+        var altColors=false;
+        var count=0;
+        var countN=0;
+        for (var ix=0; ix<rows.length; ix++) {    // Need to have buttons in order
+            var btn = $( rows[ix] ).find("p.btn");
+            var side = $( rows[ix] ).find(".sliceDiv.sideLab"); // added by GALT
+            if (btn.length === 0)
+                continue;
+            var classList = $( btn ).attr("class").split(" ");
+            var curMatchesLast=(classList[0] === lastClass);
+
+            // centerLabels may be conditionally seen
+            if ($( rows[ix] ).hasClass('clOpt')) {
+                // if same composite and previous also centerLabel optional then hide center label
+                if (curMatchesLast && $( rows[ix - 1] ).hasClass('clOpt'))
+                    dragReorder.showCenterLabel(rows[ix],false);
+                else
+                    dragReorder.showCenterLabel(rows[ix],true);
+            }
+
+            // On with buttons
+            if (lastBtn) {
+                $( lastBtn ).removeClass('btnN btnU btnL btnD');
+                if (curMatchesLast && lastMatchesLast) {
+                    $( lastBtn ).addClass('btnL');
+                    $( lastBtn ).css('height', $( lastSide ).height() - 0);  // added by GALT
+                } else if (lastMatchesLast) {
+                    $( lastBtn ).addClass('btnU');
+                    $( lastBtn ).css('height', $( lastSide ).height() - 1);  // added by GALT
+                } else if (curMatchesLast) {
+                    $( lastBtn ).addClass('btnD');
+                    $( lastBtn ).css('height', $( lastSide ).height() - 2);  // added by GALT
+                } else {
+                    $( lastBtn ).addClass('btnN');
+                    $( lastBtn ).css('height', $( lastSide ).height() - 3);  // added by GALT
+                    countN++;
+                }
+                count++;
+                if (altColors) {
+                    // lastMatch and lastBlue or not lastMatch and notLastBlue
+                    lastBlue = (lastMatchesLast === lastBlue);
+                    if (lastBlue)    // Too  smart by 1/3rd
+                        $( lastBtn ).addClass(    'btnBlue' );
+                    else
+                        $( lastBtn ).removeClass( 'btnBlue' );
+                }
+            }
+            lastMatchesLast = curMatchesLast;
+            lastClass = classList[0];
+            lastBtn = btn;
+            lastSide = side;
+        }
+        if (lastBtn) {
+            $( lastBtn ).removeClass('btnN btnU btnL btnD');
+            if (lastMatchesLast) {
+                $( lastBtn ).addClass('btnU');
+                $( lastBtn ).css('height', $( lastSide ).height() - 1);  // added by GALT
+            } else {
+                $( lastBtn ).addClass('btnN');
+                $( lastBtn ).css('height', $( lastSide ).height() - 3);  // added by GALT
+                countN++;
+            }
+            if (altColors) {
+                // lastMatch and lastBlue or not lastMatch and notLastBlue
+                lastBlue = (lastMatchesLast === lastBlue);
+                if (lastBlue)    // Too  smart by 1/3rd
+                    $( lastBtn ).addClass(    'btnBlue' );
+                else
+                    $( lastBtn ).removeClass( 'btnBlue' );
+            }
+            count++;
+        }
+        //warn("Zipped "+count+" buttons "+countN+" are independent.");
+    },
+
+    dragHandleMouseOver: function ()
+    {   // Highlights a single row when mouse over a dragHandle column (sideLabel and buttons)
+        if ( ! jQuery.tableDnD ) {
+            //var handle = $("td.dragHandle");
+            //$(handle)
+            //    .unbind('mouseenter')//, jQuery.tableDnD.mousemove);
+            //    .unbind('mouseleave');//, jQuery.tableDnD.mouseup);
+            return;
+        }
+        if ( ! jQuery.tableDnD.dragObject ) {
+            $( this ).parents("tr.trDraggable").addClass("trDrag");
+        }
+    },
+
+    dragHandleMouseOut: function ()
+    {   // Ends row highlighting by mouse over
+        $( this ).parents("tr.trDraggable").removeClass("trDrag");
+    },
+
+    buttonMouseOver: function ()
+    {   // Highlights a composite set of buttons, regarless of whether tracks are adjacent
+        if ( ! jQuery.tableDnD || ! jQuery.tableDnD.dragObject ) {
+            var classList = $( this ).attr("class").split(" ");
+            var btns = $( "p." + classList[0] );
+            $( btns ).removeClass('btnGrey');
+            $( btns ).addClass('btnBlue');
+            if (jQuery.tableDnD) {
+                var rows = dragReorder.getContiguousRowSet($(this).parents('tr.trDraggable')[0]);
+                if (rows)
+                    $( rows ).addClass("trDrag");
+            }
+        }
+    },
+
+    buttonMouseOut: function ()
+    {   // Ends composite highlighting by mouse over
+        var classList = $( this ).attr("class").split(" ");
+        var btns = $( "p." + classList[0] );
+        $( btns ).removeClass('btnBlue');
+        $( btns ).addClass('btnGrey');
+        if (jQuery.tableDnD) {
+            var rows = dragReorder.getContiguousRowSet($(this).parents('tr.trDraggable')[0]);
+            if (rows)
+            $( rows ).removeClass("trDrag");
+        }
+    },
+
+    trMouseOver: function (e)
+    {   // Trying to make sure there is always a imageV2.lastTrack so that we know where we are
+        var id = '';
+        var a = /tr_(.*)/.exec($(this).attr('id'));  // voodoo
+        if (a && a[1]) {
+            id = a[1];
+        }
+        if (id.length > 0) {
+            if ( ! imageV2.lastTrack || imageV2.lastTrack.id !== id)
+                imageV2.lastTrack = rightClick.makeMapItem(id);
+                // currentMapItem gets set by mapItemMapOver.   This is just backup
+        }
+    },
+
+    mapItemMouseOver: function ()
+    {
+        // Record data for current map area item
+        var id = this.id;
+        if (!id || id.length === 0) {
+            id = '';
+            var tr = $( this ).parents('tr.imgOrd');
+            if ( $(tr).length === 1 ) {
+                var a = /tr_(.*)/.exec($(tr).attr('id'));  // voodoo
+                if (a && a[1]) {
+                    id = a[1];
+                }
+            }
+        }
+        if (id.length > 0) {
+            rightClick.currentMapItem = rightClick.makeMapItem(id);
+            if (rightClick.currentMapItem) {
+                rightClick.currentMapItem.href = this.href;
+                rightClick.currentMapItem.title = this.title;
+
+                // Handle linked features with separate clickmaps for each exon/intron
+                if ((this.title.indexOf('Exon ') === 0) || (this.title.indexOf('Intron ') === 0)) {
+                    // if the title is Exon ... or Intron ...
+                    // then search for the sibling with the same href
+                    // that has the real title item label
+                    var elem = this.parentNode.firstChild;
+                    while (elem) {
+                        if ((elem.href === this.href)
+                            && !((elem.title.indexOf('Exon ') === 0) || (elem.title.indexOf('Intron ') === 0))) {
+                            rightClick.currentMapItem.title = elem.title;
+                            break;
+                        }
+                        elem = elem.nextSibling;
+                    }
+                }
+
+            }
+        }
+    },
+
+    mapItemMouseOut: function ()
+    {
+        imageV2.lastTrack = rightClick.currentMapItem; // Just a backup
+        rightClick.currentMapItem = null;
+    },
+
+    init: function ()
+    {   // Make side buttons visible (must also be called when updating rows in the imgTbl).
+        var btns = $("p.btn");
+        if (btns.length > 0) {
+            dragReorder.zipButtons($('#imgTbl'));
+            $(btns).mouseenter( dragReorder.buttonMouseOver );
+            $(btns).mouseleave( dragReorder.buttonMouseOut  );
+            $(btns).show();
+        }
+        var handle = $("td.dragHandle");
+        if (handle.length > 0) {
+            $(handle).mouseenter( dragReorder.dragHandleMouseOver );
+            $(handle).mouseleave( dragReorder.dragHandleMouseOut  );
+        }
+
+        // setup mouse callbacks for the area tags
+        $("#imgTbl").find("tr").mouseover( dragReorder.trMouseOver );
+
+
+        $(".area").each( function(t) {
+                            this.onmouseover = dragReorder.mapItemMouseOver;
+                            this.onmouseout = dragReorder.mapItemMouseOut;
+                            this.onclick = posting.mapClk;
+                        });
+    }
+};