e1ba0aaa1baec345d56cc8a518b8450c3e5d74c3 tdreszer Wed Jan 22 14:31:22 2014 -0800 Checking in new feature 'drag-select highlight', which was originally coded by Larry. Redmine #709 (been on the shelf for awhile). diff --git src/hg/js/hgTracks.js src/hg/js/hgTracks.js index b4b2af5..514edb2 100644 --- src/hg/js/hgTracks.js +++ src/hg/js/hgTracks.js @@ -351,33 +351,48 @@ else minPixels = num; } return ( movedX > minPixels || movedX < (minPixels * -1) || movedY > minPixels || movedY < (minPixels * -1)); } } ///////////////// //// posting //// ///////////////// var posting = { blockUseMap: false, - blockMapClicks: function () { posting.blockUseMap=true; }, - allowMapClicks: function () { posting.blockUseMap=false; }, - mapClicksAllowed: function () { return (posting.blockUseMap == false); }, + blockMapClicks: function () + // Blocks clicking on map items when in effect. Drag opperations frequently call this. + { + posting.blockUseMap=true; + }, + + allowMapClicks:function () + // reallows map clicks. Called after operations that compete with clicks (e.g. dragging) + { + $('body').css('cursor', ''); // Explicitly remove wait cursor. + posting.blockUseMap=false; + }, + + mapClicksAllowed: function () + // Verify that click-competing operation (e.g. dragging) isn't currently occurring. + { + return (posting.blockUseMap == false); + }, blockTheMapOnMouseMove: function (ev) { if (!posting.blockUseMap && mouse.hasMoved(ev)) { posting.blockUseMap=true; } }, mapClk: function () { var done = false; if(false && imageV2.inPlaceUpdate) { // XXXX experimental and only turned on in larrym's tree. // Use in-place update if the map item just modifies the current position (this is nice because it's faster // and it preserves the users current relative position in the track image). @@ -533,121 +548,208 @@ var form = $('form#TrackForm'); $(form).submit(function () { $('select.normalText,select.hiddenText').attr('disabled',true); }); $(form).attr('method','get'); } } //////////////////////////////////////////////////////////// // dragSelect is also known as dragZoom or shift-dragZoom // //////////////////////////////////////////////////////////// var dragSelect = { areaSelector: null, // formerly "imgAreaSelect". jQuery element used for imgAreaSelect - autoHideSetting: true, // Current state of imgAreaSelect autoHide setting originalCursor: null, startTime: null, - rulerModeToggle: function (ele) // UNUSED? - { - dragSelect.autoHideSetting = !ele.checked; - var obj = dragSelect.areaSelector.data('imgAreaSelect'); - obj.setOptions({autoHide : dragSelect.autoHideSetting}); - }, - selectStart: function (img, selection) { initVars(); if(rightClick.menu) { rightClick.menu.hide(); } var now = new Date(); dragSelect.startTime = now.getTime(); posting.blockMapClicks(); }, selectChange: function (img, selection) { if(selection.x1 != selection.x2) { if (genomePos.check(img, selection)) { genomePos.update(img, selection, false); jQuery('body').css('cursor', dragSelect.originalCursor); } else { jQuery('body').css('cursor', 'not-allowed'); } } return true; }, + highlightThisRegion: function(newPosition) + // set highlighting newPosition in server-side cart and apply the highlighting in local UI. + { + var start, end; + if (arguments.length == 2) { + start = arguments[0]; + end = arguments[1]; + } else { + var pos = parsePosition(newPosition); + start = pos.start; + end = pos.end; + } + hgTracks.highlight = getDb() + "." + hgTracks.chromName + ":" + start + "-" + end; + hgTracks.highlight += '#AAFFFF'; // Also include highlight color + // we include enableHighlightingDialog because it may have been changed by the dialog + setCartVars(['highlight', 'enableHighlightingDialog'], + [hgTracks.highlight, hgTracks.enableHighlightingDialog ? 1 : 0]); + imageV2.highlightRegion(); + }, + + selectionEndDialog: function (newPosition) + // Let user choose between zoom-in and highlighting. + { + var dragSelectDialog = $("#dragSelectDialog")[0]; + if (!dragSelectDialog) { + $("body").append("<div id='dragSelectDialog'>" + newPosition + + "<p><input type='checkbox' id='disableDragHighlight'>" + + "Don't show this dialog again and always zoom.<BR>" + + "(Re-enable highlight via the 'configure' menu at any time.)</p>"); + dragSelectDialog = $("#dragSelectDialog")[0]; + } + $(dragSelectDialog).dialog({ + modal: true, + title: "Drag-and-select", + closeOnEscape: true, + resizable: false, + autoOpen: false, + revertToOriginalPos: true, + minWidth: 400, + buttons: { + "Zoom In": function() { + // Zoom to selection + $(this).dialog("option", "revertToOriginalPos", false); + if (imageV2.inPlaceUpdate) { + var params = "position=" + newPosition; + if ($("#disableDragHighlight").attr('checked')) { + hgTracks.enableHighlightingDialog = false; + params += "&enableHighlightingDialog=0" + } + imageV2.navigateInPlace(params, null, true); + } else { + $('body').css('cursor', 'wait'); + document.TrackHeaderForm.submit(); + } + $(this).dialog("close"); + }, + "Highlight": function() { + // Highlight selection + $(imageV2.imgTbl).imgAreaSelect({hide:true}); + if ($("#disableDragHighlight").attr('checked')) + hgTracks.enableHighlightingDialog = false; + dragSelect.highlightThisRegion(newPosition); + $(this).dialog("close"); + }, + "Cancel": function() { + $(this).dialog("close"); + } + }, + open: function () { // Make zoom the focus/default action + $(this).parents('.ui-dialog-buttonpane button:eq(0)').focus(); + }, + + close: function() { + // All exits to dialog should go through this + $(imageV2.imgTbl).imgAreaSelect({hide:true}); + if($(this).dialog("option", "revertToOriginalPos")) + genomePos.revertToOriginalPos(); + if($("#disableDragHighlight").attr('checked')) + $(this).remove(); + else + $(this).hide(); + $('body').css('cursor', ''); // Occasionally wait cursor got left behind + } + }); + $(dragSelectDialog).dialog('open'); + }, + selectEnd: function (img, selection) { var now = new Date(); var doIt = false; if(dragSelect.originalCursor != null) jQuery('body').css('cursor', dragSelect.originalCursor); // ignore releases outside of the image rectangle (allowing a 10 pixel slop) - if(dragSelect.autoHideSetting && genomePos.check(img, selection)) { - // ignore single clicks that aren't in the top of the image (this happens b/c the clickClipHeight test in dragSelect.selectStart + if (genomePos.check(img, selection)) { + // ignore single clicks that aren't in the top of the image + // (this happens b/c the clickClipHeight test in dragSelect.selectStart // doesn't occur when the user single clicks). doIt = dragSelect.startTime != null || selection.y1 <= hgTracks.rulerClickHeight; } if(doIt) { // dragSelect.startTime is null if mouse has never been moved var singleClick = ( (selection.x2 == selection.x1) || dragSelect.startTime == null || (now.getTime() - dragSelect.startTime) < 100); var newPosition = genomePos.update(img, selection, singleClick); if(newPosition != undefined) { + if (hgTracks.enableHighlightingDialog) + dragSelect.selectionEndDialog(newPosition); + else { + $(imageV2.imgTbl).imgAreaSelect({hide:true}); if (imageV2.inPlaceUpdate) { imageV2.navigateInPlace("position=" + newPosition, null, true); } else { jQuery('body').css('cursor', 'wait'); document.TrackHeaderForm.submit(); } } + } } else { + $(imageV2.imgTbl).imgAreaSelect({hide:true}); genomePos.revertToOriginalPos(); } dragSelect.startTime = null; - setTimeout('posting.allowMapClicks();',50); // Necessary incase the dragSelect.selectEnd was over a map item. select takes precedence. + // blockMapClicks/allowMapClicks() is necessary if selectEnd was over a map item. + setTimeout('posting.allowMapClicks();',50); return true; }, load: function (firstTime) { var imgHeight = 0; if (imageV2.enabled) - imgHeight = imageV2.imgTbl.height(); + imgHeight = imageV2.imgTbl.innerHeight() - 1; // last little bit makes border look ok // No longer disable without ruler, because shift-drag still works if(typeof(hgTracks) != "undefined") { if (hgTracks.rulerClickHeight == undefined || hgTracks.rulerClickHeight == null) hgTracks.rulerClickHeight = 0; // will be zero if no ruler track var heights = hgTracks.rulerClickHeight; dragSelect.areaSelector = jQuery((imageV2.imgTbl).imgAreaSelect({ selectionColor: 'blue', outerColor: '', minHeight: imgHeight, maxHeight: imgHeight, onSelectStart: dragSelect.selectStart, onSelectChange: dragSelect.selectChange, onSelectEnd: dragSelect.selectEnd, - autoHide: dragSelect.autoHideSetting, + autoHide: false, // gets hidden after possible dialog movable: false, clickClipHeight: heights })); } } } ///////////////////////////////////// //// Chrom Drag/Zoom/Expand code //// ///////////////////////////////////// jQuery.fn.chromDrag = function(){ this.each(function(){ // Plan: // mouseDown: determine where in map: convert to img location: pxDown // mouseMove: flag drag @@ -1308,51 +1410,53 @@ } ////////////////////////// //// Drag Scroll code //// ////////////////////////// jQuery.fn.panImages = function(){ // globals across all panImages genomePos.original = genomePos.getOriginalPos(); // XXXX what is this for? (this already happened in initVars). var leftLimit = hgTracks.imgBoxLeftLabel * -1; var rightLimit = (hgTracks.imgBoxPortalWidth - hgTracks.imgBoxWidth + leftLimit); var only1xScrolling = ((hgTracks.imgBoxWidth - hgTracks.imgBoxPortalWidth) == 0);//< hgTracks.imgBoxLeftLabel); var prevX = (hgTracks.imgBoxPortalOffsetX + hgTracks.imgBoxLeftLabel) * -1; var portalWidth = 0; var savedPosition; + var highlightArea = null; // Used to ensure dragSelect highlight will scroll. this.each(function(){ var pic; var pan; if ( $(this).is("img") ) { pan = $(this).parent("div"); pic = $(this); } else if ( $(this).is("div.scroller") ) { pan = $(this); pic = $(this).children("img#panImg"); // Get the real pic } if(pan == undefined || pic == undefined) { throw "Not a div with a child image! 'panImages' can only be used with divs contain images."; } // globals across all panImages portalWidth = $(pan).width(); + portalAbsoluteX = $(pan).offset().left; // globals to one panImage var newX = 0; var mouseDownX = 0; var mouseIsDown = false; var beyondImage = false; var atEdge = false; initialize(); function initialize(){ $(pan).parents('td.tdData').mousemove(function(e) { if (e.shiftKey) $(this).css('cursor',"crosshair"); // shift-dragZoom else if ( $.browser.msie ) // IE will override map item cursors if this gets set @@ -1360,30 +1464,31 @@ //else // NOTE: Open hand cursor is being removed because it makes vis toggling less obvious // $(this).css('cursor',"url(../images/grabber.cur),w-resize"); // dragScroll }); panAdjustHeight(prevX); pan.mousedown(function(e){ if (e.which > 1 || e.button > 1 || e.shiftKey) return true; if(mouseIsDown == false) { if(rightClick.menu) { rightClick.menu.hide(); } mouseIsDown = true; mouseDownX = e.clientX; + highlightArea = $('#highlightItem')[0]; atEdge = (!beyondImage && (prevX >= leftLimit || prevX <= rightLimit)); $(document).bind('mousemove',panner); $(document).bind( 'mouseup', panMouseUp); // Will exec only once return false; } }); } function panner(e) { //if (!e) e = window.event; if ( mouseIsDown ) { var relativeX = (e.clientX - mouseDownX); if(relativeX != 0) { if (posting.mapClicksAllowed()) { @@ -1409,58 +1514,61 @@ if(atEdge) { // Do not drag straight off edge. Force second drag beyondImage = true; newX = rightLimit - (rightLimit - newX)/decelerator;// slower //if (newX < rightLimit - wingSize) // Don't go too far over the edge! // newX = rightLimit - wingSize; } else newX = rightLimit; } else if(newX >= rightLimit && newX < leftLimit) beyondImage = false; // could have scrolled back without mouse up newX = panUpdatePosition(newX,true); var nowPos = newX.toString() + "px"; $(".panImg").css( {'left': nowPos }); $('.tdData').css( {'backgroundPosition': nowPos } ); + scrollHighlight(relativeX); if (!only1xScrolling) panAdjustHeight(newX); // NOTE: This will dynamically resize image while scrolling. Do we want to? } } } function panMouseUp(e) { // Must be a separate function instead of pan.mouseup event. //if (!e) e = window.event; if(mouseIsDown) { dragMaskClear(); $(document).unbind('mousemove',panner); $(document).unbind('mouseup',panMouseUp); mouseIsDown = false; setTimeout('posting.allowMapClicks();',50); // Necessary incase the dragSelect.selectEnd was over a map item. select takes precedence. // Outside image? Then abandon. var curY = e.pageY; var imgTbl = $('#imgTbl'); var north = $(imgTbl).offset().top; var south = north + $(imgTbl).height(); if (curY < north || curY > south) { atEdge = false; beyondImage = false; if (savedPosition != undefined) genomePos.set(savedPosition,null); var oldPos = prevX.toString() + "px"; $(".panImg").css( {'left': oldPos }); $('.tdData').css( {'backgroundPosition': oldPos } ); + if (highlightArea) + imageV2.highlightRegion(); return true; } // Do we need to fetch anything? if(beyondImage) { if(imageV2.inPlaceUpdate) { var pos = parsePosition(genomePos.get()); imageV2.navigateInPlace("position=" + encodeURIComponent(pos.chrom + ":" + pos.start + "-" + pos.end), null, true); } else { document.TrackHeaderForm.submit(); } return true; // Make sure the setTimeout below is not called. } @@ -1618,30 +1726,48 @@ var imgTbl = $('#imgTbl'); // Find or create the waitMask (which masks the whole page) var dragMask = $('div#dragMask'); if (dragMask != undefined && dragMask.length >= 1) { $(dragMask).height( $(imgTbl).height() ); } } function dragMaskClear() { // Clears the dragMask $('body').css('cursor','auto') var dragMask = $('#dragMask'); if (dragMask != undefined ) $(dragMask).hide(); } + function scrollHighlight(relativeX) + // Scrolls the highlight region if one exists + { + if (highlightArea) { + // Best to have a left and right, then min/max the edges, then set width + var hiOffset = $(highlightArea).offset(); + var hiDefinedLeft = $(highlightArea).data('leftPixels'); + var hiDefinedWidth = $(highlightArea).data('widthPixels'); + hiOffset.left = Math.max(hiDefinedLeft + relativeX, + portalAbsoluteX); + var right = Math.min(hiDefinedLeft + hiDefinedWidth + relativeX, + portalAbsoluteX + portalWidth); + var newWidth = Math.max(right - hiOffset.left,0); + if (hiDefinedWidth != newWidth) + $(highlightArea).width(newWidth); + $(highlightArea).offset(hiOffset); + } + } }; /////////////////////////////////////// //// rightClick (aka context menu) //// /////////////////////////////////////// var rightClick = { menu: null, selectedMenuItem: null, // currently choosen context menu item (via context menu). floatingMenuItem: null, currentMapItem: null, supportZoomCodon: false, // turn on experimental zoom-to-codon functionality (currently only on in larry's tree). @@ -1693,31 +1819,31 @@ genomePos.set(json.pos, 3); if(document.TrackForm) document.TrackForm.submit(); else document.TrackHeaderForm.submit(); } else { alert(json.error); } }, handleViewImg: function (response, status) { // handles view image response, which must get new image without imageV2 gimmickery jQuery('body').css('cursor', ''); var str = "<IMG[^>]*SRC='([^']+)'"; var reg = new RegExp(str); - a = reg.exec(response); + var a = reg.exec(response); if(a && a[1]) { if(window.open(a[1]) == null) { rightClick.windowOpenFailedMsg(); } return; } warn("Couldn't parse out img src"); }, myPrompt: function (msg, callback) { // replacement for prompt; avoids misleading/confusing security warnings which are caused by prompt in IE 7+ // callback is called if user presses "OK". $("body").append("<div id = 'myPrompt'><div id='dialog' title='Basic dialog'><form>" + msg + "<input id='myPromptText' value=''></form>"); $("#myPrompt").dialog({ @@ -1736,31 +1862,31 @@ { setTimeout( function() { rightClick.hitFinish(menuItemClicked, menuObject, cmd, args); }, 1); }, hitFinish: function (menuItemClicked, menuObject, cmd, args) { // dispatcher for context menu hits var id = rightClick.selectedMenuItem.id; var url = null; if(menuObject.shown) { // warn("Spinning: menu is still shown"); setTimeout(function() { rightClick.hitFinish(menuItemClicked, menuObject, cmd); }, 10); return; } - if(cmd == 'selectWholeGene' || cmd == 'getDna') { + if (cmd == 'selectWholeGene' || cmd == 'getDna' || cmd == 'highlightItem') { // bring whole gene into view or redirect to DNA screen. var href = rightClick.selectedMenuItem.href; var chromStart, chromEnd; var a = /hgg_chrom=(\w+)&/.exec(href); // Many links leave out the chrom (b/c it's in the server side cart as "c") var chrom = hgTracks.chromName; if(a) { if(a && a[1]) chrom = a[1]; a = /hgg_start=(\d+)/.exec(href); if(a && a[1]) chromStart = parseInt(a[1]) + 1; a = /hgg_end=(\d+)/.exec(href); if(a && a[1]) chromEnd = parseInt(a[1]); @@ -1775,30 +1901,32 @@ a = /t=(\d+)/.exec(href); if(a && a[1]) chromEnd = parseInt(a[1]); } if(chrom == null || chromStart == null || chromEnd == null) { warn("couldn't parse out genomic coordinates"); } else { if(cmd == 'getDna') { // NOTE: this should be shared with URL generation for getDna blue bar menu url = "../cgi-bin/hgc?g=getDna&i=mixed&c=" + chrom; url += "&l=" + (chromStart - 1) + "&r=" + chromEnd; url += "&db=" + getDb() + "&hgsid=" + getHgsid(); if (window.open(url) === null) { rightClick.windowOpenFailedMsg(); } + } else if (cmd == 'highlightItem') { + dragSelect.highlightThisRegion(parseInt(chromStart), parseInt(chromEnd)); } else { var newPosition = genomePos.setByCoordinates(chrom, chromStart, chromEnd); var reg = new RegExp("hgg_gene=([^&]+)"); var a = reg.exec(href); var name; // pull item name out of the url so we can set hgFind.matches (redmine 3062) if(a && a[1]) { name = a[1]; } else { reg = new RegExp("[&?]i=([^&]+)"); a = reg.exec(href); if(a && a[1]) { name = a[1]; } } @@ -1867,38 +1995,30 @@ } else if (cmd == 'hgTrackUi_follow') { var url = "hgTrackUi?hgsid=" + getHgsid() + "&g="; var rec = hgTracks.trackDb[id]; if (tdbHasParent(rec) && tdbIsLeaf(rec)) url += rec.parentTrack else { var link = $( 'td#td_btn_'+ rightClick.selectedMenuItem.id ).children('a'); // The button already has the ref if ($(link) != undefined) url = $(link).attr('href'); else url += rightClick.selectedMenuItem.id; } location.assign(url); - } else if (cmd == 'dragZoomMode') { - dragSelect.autoHideSetting = true; - var obj = dragSelect.areaSelector.data('imgAreaSelect'); - obj.setOptions({autoHide : true, movable: false}); - } else if (cmd == 'hilightMode') { - dragSelect.autoHideSetting = false; - var obj = dragSelect.areaSelector.data('imgAreaSelect'); - obj.setOptions({autoHide : false, movable: true}); } else if (cmd == 'viewImg') { // Fetch a new copy of track img and show it to the user in another window. This code assume we have updated // remote cart with all relevant chages (e.g. drag-reorder). /* Here's how to do this more directly with hgRenderTracks: if(window.open("../cgi-bin/hgRenderTracks?hgt.internal=1&hgsid=" + getHgsid()) == null) { rightClick.windowOpenFailedMsg(); } return; */ var data = "hgt.imageV1=1&hgt.trackImgOnly=1&hgsid=" + getHgsid(); jQuery('body').css('cursor', 'wait'); $.ajax({ type: "GET", url: "../cgi-bin/hgTracks", data: data, @@ -1960,75 +2080,87 @@ var rows = dragReorder.getContiguousRowSet(row); if (rows && rows.length > 0) { var vars = new Array(); var vals = new Array(); for (var ix=rows.length - 1; ix >= 0; ix--) { // from bottom, just in case remove screws with us var rowId = $(rows[ix]).attr('id').substring('tr_'.length); //if (tdbIsSubtrack(hgTracks.trackDb[rowId]) == false) // warn('What went wrong?'); vars.push(rowId, rowId+'_sel'); // Remove subtrack level vis and explicitly uncheck. vals.push('[]', 0); $(rows[ix]).remove(); } if (vars.length > 0) { setCartVars( vars, vals ); - dragReorder.init(); - dragSelect.load(false); } - imageV2.markAsDirtyPage(); + imageV2.afterImgChange(true); } } else if (cmd == 'hideComposite') { var rec = hgTracks.trackDb[id]; if (tdbIsSubtrack(rec)) { var row = $( 'tr#tr_' + id ); var rows = dragReorder.getCompositeSet(row); if (rows && rows.length > 0) { for (var ix=rows.length - 1; ix >= 0; ix--) { // from bottom, just in case remove screws with us $(rows[ix]).remove(); } var selectUpdated = vis.update(rec.parentTrack, 'hide'); setCartVar(rec.parentTrack, 'hide' ); - dragReorder.init(); - dragSelect.load(false); - imageV2.markAsDirtyPage(); + imageV2.afterImgChange(true); } } //else // warn('What went wrong?'); + } else if (cmd == 'zoomToHighlight') { // If highlight exists for this assembly, zoom to it + if (hgTracks.highlight) { + var pos = parsePositionWithDb(hgTracks.highlight); + if (pos && pos.db == getDb()) { + if (imageV2.inPlaceUpdate) { + var params = "position=" + pos.chrom + ':' + pos.start + '-' + pos.end; + imageV2.navigateInPlace(params, null, true); + } else { + genomePos.setByCoordinates(pos.chrom, pos.start, pos.end); + jQuery('body').css('cursor', 'wait'); + document.TrackHeaderForm.submit(); + } + } + } + } else if (cmd == 'removeHighlight') { + hgTracks.highlight = null; + setCartVars(['highlight'], ['[]']); + imageV2.highlightRegion(); } else { // if ( cmd in 'hide','dense','squish','pack','full','show' ) // Change visibility settings: // // First change the select on our form: var rec = hgTracks.trackDb[id]; var selectUpdated = vis.update(id, cmd); // Now change the track image if(imageV2.enabled && cmd == 'hide') { // Hide local display of this track and update server side cart. // Subtracks controlled by 2 settings so del vis and set sel=0. Others, just set vis hide. if(tdbIsSubtrack(rec)) setCartVars( [ id, id+"_sel" ], [ '[]', 0 ] ); // Remove subtrack level vis and explicitly uncheck. else if(tdbIsFolderContent(rec)) setCartVars( [ id, id+"_sel" ], [ 'hide', 0 ] ); // supertrack children need to have _sel set to trigger superttrack reshaping else setCartVar(id, 'hide' ); $(document.getElementById('tr_' + id)).remove(); - dragReorder.init(); - dragSelect.load(false); - imageV2.markAsDirtyPage(); + imageV2.afterImgChange(true); } else if (!imageV2.mapIsUpdateable) { jQuery('body').css('cursor', 'wait'); if(selectUpdated) { // assert(document.TrackForm); document.TrackForm.submit(); } else { // add a hidden with new visibility value var form = $(document.TrackHeaderForm); $("<input type='hidden' name='"+id+"'value='"+cmd+"'>").appendTo(form); document.TrackHeaderForm.submit(); } } else { imageV2.requestImgUpdate(id, id + "=" + cmd, "", cmd); } } @@ -2189,30 +2321,37 @@ if(rec.type.indexOf("wig") == 0 || rec.type.indexOf("bigWig") == 0 || id == "wikiTrack") { displayItemFunctions = false; } else if(rec.type.indexOf("expRatio") == 0) { displayItemFunctions = title != "zoomInMore"; } else { displayItemFunctions = true; } } if(displayItemFunctions) { o[rightClick.makeImgTag("magnify.png") + " Zoom to " + title] = { onclick: function(menuItemClicked, menuObject) { rightClick.hit(menuItemClicked, menuObject, "selectWholeGene"); return true; } }; + o[rightClick.makeImgTag("highlight.png") + " Highlight " + title] = + { onclick: function(menuItemClicked, menuObject) { + rightClick.hit(menuItemClicked, menuObject, + "highlightItem"); + return true; + } + }; if(rightClick.supportZoomCodon && rec.type.indexOf("genePred") != -1) { // http://hgwdev-larrym.cse.ucsc.edu/cgi-bin/hgGene?hgg_gene=uc003tqk.2&hgg_prot=P00533&hgg_chrom=chr7&hgg_start=55086724&hgg_end=55275030&hgg_type=knownGene&db=hg19&c=chr7 var name, table; var reg = new RegExp("hgg_gene=([^&]+)"); var a = reg.exec(href); if(a && a[1]) { name = a[1]; reg = new RegExp("hgg_type=([^&]+)"); a = reg.exec(href); if(a && a[1]) { table = a[1]; } } else { // http://hgwdev-larrym.cse.ucsc.edu/cgi-bin/hgc?o=55086724&t=55275031&g=refGene&i=NM_005228&c=chr7 // http://hgwdev-larrym.cse.ucsc.edu/cgi-bin/hgc?o=55086713&t=55270769&g=wgEncodeGencodeManualV4&i=ENST00000455089&c=chr7 @@ -2273,59 +2412,30 @@ item = rightClick.makeImgTag("book.png")+" Show details for "+ title + "..."; o[item] = {onclick: function(menuItemClicked, menuObject) { rightClick.hit(menuItemClicked,menuObject,"followLink"); return true; } }; any = true; } } if(any) { menu.push($.contextMenu.separator); menu.push(o); } } } - if (!done) { - if(false) { - // Currently toggling b/n drag-and-zoom mode and hilite mode is disabled b/c we don't know how to keep hilite mode from disabling the - // context menus. - var o = new Object(); - var str = "drag-and-zoom mode"; - if(dragSelect.autoHideSetting) { - str += selectedImg; - // menu[str].className = 'context-menu-checked-item'; - } - o[str] = { onclick: function(menuItemClicked, menuObject) { - rightClick.hit(menuItemClicked, menuObject, "dragZoomMode"); - return true; } - }; - menu.push(o); - o = new Object(); - // console.dir(ele); - str = "hilight mode"; - if (!dragSelect.autoHideSetting) { - str += selectedImg; - } - o[str] = { onclick: function(menuItemClicked, menuObject) { - rightClick.hit(menuItemClicked, menuObject, "hilightMode"); - return true; } - }; - menu.push(o); - } - //menu.push({"view image": {onclick: function(menuItemClicked, menuObject) { rightClick.hit(menuItemClicked, menuObject, "viewImg"); return true; }}}); - } if(rightClick.selectedMenuItem && rec) { // Add cfg options at just shy of end... var o = new Object(); if(tdbIsLeaf(rec)) { if (rec["configureBy"] != 'none' && (!tdbIsCompositeSubtrack(rec) || rec["configureBy"] != 'clickThrough')) { // Note that subtracks never do clickThrough because // parentTrack cfg is the desired clickThrough o[rightClick.makeImgTag("wrench.png")+" Configure "+rec.shortLabel] = { onclick: function(menuItemClicked, menuObject) { rightClick.hit(menuItemClicked, menuObject, "hgTrackUi_popup"); return true; } }; @@ -2347,38 +2457,58 @@ return true; } }; } if(jQuery.floatMgr) { o[(rightClick.selectedMenuItem.id == rightClick.floatingMenuItem ? selectedImg : blankImg) + " float"] = { onclick: function(menuItemClicked, menuObject) { rightClick.hit(menuItemClicked, menuObject, "float"); return true; } }; } menu.push($.contextMenu.separator); menu.push(o); } + menu.push($.contextMenu.separator); + if(hgTracks.highlight) { + var o; + if (hgTracks.highlight.search(getDb() + '.') == 0) { + o = new Object(); + o[rightClick.makeImgTag("highlightZoom.png") + + " Zoom to highlighted region"] = + { onclick:function(menuItemClicked, menuObject) { + rightClick.hit(menuItemClicked, menuObject, "zoomToHighlight"); + return true; + } + }; + o[rightClick.makeImgTag("highlightRemove.png") + " Remove highlighting"] = + { onclick:function(menuItemClicked, menuObject) { + rightClick.hit(menuItemClicked, menuObject, "removeHighlight"); + return true; + } + }; + menu.push(o); + } + } // Add view image at end var o = new Object(); o[rightClick.makeImgTag("eye.png") + " View image"] = { onclick: function(menuItemClicked, menuObject) { rightClick.hit(menuItemClicked, menuObject, "viewImg"); return true; } }; - menu.push($.contextMenu.separator); menu.push(o); return menu; }, { beforeShow: function(e) { // console.log(mapItems[rightClick.selectedMenuItem]); rightClick.selectedMenuItem = rightClick.findMapItem(e); // XXXX? posting.blockUseMap = true; return true; }, hideTransition:'hide', // hideCallback fails if these are not defined. hideSpeed:10, hideCallback: function() { $('p.btn.blueButtons').removeClass('blueButtons'); @@ -2451,32 +2581,31 @@ var rec = hgTracks.trackDb[trackName]; var subtrack = tdbIsSubtrack(rec) ? trackName :undefined; // If subtrack then vis rules differ var allVars = getAllVars($('#hgTrackUiDialog'), subtrack );// For unknown reasons IE8 fails to find $('#pop'), occasionally var changedVars = varHashChanges(allVars,popUp.saveAllVars); //warn("cfgVars:"+varHashToQueryString(changedVars)); var newVis = changedVars[trackName]; var hide = (newVis != null && (newVis == 'hide' || newVis == '[]')); // subtracks do not have "hide", thus '[]' if($('#imgTbl') == undefined) { // On findTracks or config page setVarsFromHash(changedVars); //if(hide) // TODO: When findTracks or config page has cfg popup, then vis change needs to be handled in page here } else { // On image page if(hide) { setVarsFromHash(changedVars); $(document.getElementById('tr_' + trackName)).remove(); - dragReorder.init(); - dragSelect.load(false); + imageV2.afterImgChange(true); } else { // Keep local state in sync if user changed visibility if(newVis != null) { vis.update(trackName, newVis); } var urlData = varHashToQueryString(changedVars); if(urlData.length > 0) { if(imageV2.mapIsUpdateable) { imageV2.requestImgUpdate(trackName,urlData,""); } else { window.location = "../cgi-bin/hgTracks?" + urlData + "&hgsid=" + getHgsid(); } } } @@ -2646,53 +2775,64 @@ }, function (position) { genomePos.set(position, commify(getSizeFromCoordinates(position))); }); // Make sure suggestTrack is visible when user chooses something via gene select (#3484). if($("#suggestTrack").length) { $(document.TrackForm || document.TrackHeaderForm).submit(function(event) { if($('#hgFindMatches').length) { vis.makeTrackVisible($("#suggestTrack").val()); } }); } } }, + afterImgChange: function (dirty) + { // Standard things to do when manipulations change image without ajax update. + dragReorder.init(); + dragSelect.load(false); + imageV2.highlightRegion(); + if (dirty) + imageV2.markAsDirtyPage(); + }, + afterReload: function (id) { // Reload various UI widgets after updating imgTbl map. dragReorder.init(); dragSelect.load(false); // Do NOT reload context menu (otherwise we get the "context menu sticks" problem). // rightClick.load($('#tr_' + id)); if(imageV2.imgTbl.tableDnDUpdate) imageV2.imgTbl.tableDnDUpdate(); rightClick.reloadFloatingItem(); // Turn on drag scrolling. if(hgTracks.imgBoxPortal) { $("div.scroller").panImages(); } if (imageV2.backSupport) { if (id) { // The remainder is only needed for full reload imageV2.markAsDirtyPage(); // vis of cfg change + imageV2.highlightRegion(); return; } } imageV2.loadRemoteTracks(); makeItemsByDrag.load(); imageV2.loadSuggestBox(); + imageV2.highlightRegion(); if (imageV2.backSupport) { imageV2.setInHistory(false); // Set this new position into History stack } else { imageV2.markAsDirtyPage(); } }, updateImgForId: function (html, id, fullImageReload, newJsonRec) { // update row in imgTbl for given id. // return true if we successfully pull slice for id and update it in imgTrack. var str = "<TR id='tr_" + id + "'[^>]*>([\\s\\S]+?)</TR>"; var reg = new RegExp(str); var a = reg.exec(html); if(a && a[1]) { @@ -3067,30 +3207,80 @@ url: "../cgi-bin/hgTracks", data: params + "&hgt.trackImgOnly=1&hgt.ideogramToo=1&hgsid=" + getHgsid(), dataType: "html", trueSuccess: imageV2.updateImgAndMap, success: catchErrorOrDispatch, error: errorHandler, cmd: 'wholeImage', loadingId: showLoadingImage("imgTbl"), disabledEle: disabledEle, currentId: currentId, currentIdYOffset: currentIdYOffset, cache: false }); }, + highlightRegion: function() + // highlight vertical region in imgTbl based on hgTracks.highlight (#709). + { + var pos; + var hexColor = '#FFAAAA' + $('#highlightItem').remove(); + if(hgTracks.highlight) { + pos = parsePositionWithDb(hgTracks.highlight); + if(pos) { + pos.start--; // make start 0-based to match hgTracks.winStart + if (pos.color) + hexColor = pos.color; + } + } + if(pos != null && pos.chrom == hgTracks.chromName && pos.db == getDb() + && pos.start <= hgTracks.imgBoxPortalEnd && pos.end >= hgTracks.imgBoxPortalStart) { + var portalWidthBases = hgTracks.imgBoxPortalEnd - hgTracks.imgBoxPortalStart; + var portal = $('#imgTbl td.tdData')[0]; + var leftPixels = $(portal).offset().left + 3; // 3 for borders and cgi item calcs ?? + var pixelsPerBase = ($(portal).width() - 2) / portalWidthBases; + var clippedStartBases = Math.max(pos.start, hgTracks.imgBoxPortalStart); + var clippedEndBases = Math.min(pos.end, hgTracks.imgBoxPortalEnd); + var widthPixels = (clippedEndBases - clippedStartBases) * pixelsPerBase; + if(hgTracks.revCmplDisp) + leftPixels += (hgTracks.imgBoxPortalEnd - clippedEndBases) * pixelsPerBase - 1; + else + leftPixels += (clippedStartBases - hgTracks.imgBoxPortalStart) * pixelsPerBase; + // Impossible to get perfect... Okay to overrun by a pixel on each side + leftPixels = Math.floor(leftPixels); + widthPixels = Math.ceil(widthPixels); + if (widthPixels < 2) { + widthPixels = 3; + leftPixels -= 1; + } + + var area = jQuery("<div id='highlightItem' class='highlightItem'></div>"); + $(area).css({ backgroundColor: hexColor, // display: 'none' + left: leftPixels + 'px', top: $('#imgTbl').offset().top + 1 + 'px', + width: widthPixels + 'px', + height: $('#imgTbl').css('height') }); + $(area).data({leftPixels: leftPixels, widthPixels: widthPixels});// needed for dragScroll + + // Larry originally appended to imgTbl, but discovered that doesn't work on IE 8 and 9. + $('body').append($(area)); + // z-index is done in css class, so highlight is beneath transparent data images. + // NOTE: ideally highlight would be below transparent blue-lines, but THAT is a + // background-image so z-index can't get below it! PS/PDF looks better for blue-lines! + } + }, + backSupport: (window.History.enabled != undefined), // support of r back button via: history: null, // jquery.history.js and HTML5 history API setupHistory: function () { // Support for back-button using jquery.history.js. // Sets up the history and initializes a state. // Since ajax updates leave the browser cached pages different from the server state, // simple back-button fails. Using a 'dirty flag' we had forced an update from server, // whenever the back button was hit, meaning there was no going back from server-state! // NOW using the hitsory API, the back-button triggers a 'statechange' event which can // contain data. We save the position in the data and ajax update the image when the // back-button is pressed. This works great for going back through ajax-updated position // changes, but is a bit messier when going back past a full-page retrieved state (as // described below). @@ -3330,30 +3520,31 @@ // Turn on drag scrolling. $("div.scroller").panImages(); } //$("#zoomSlider").slider({ min: -4, max: 3, step: 1 });//, handle: '.ui-slider-handle' }); // Retrieve tracks via AJAX that may take too long to draw initialliy (i.e. a remote bigWig) var retrievables = $('#imgTbl').find("tr.mustRetrieve") if($(retrievables).length > 0) { $(retrievables).each( function (i) { var trackName = $(this).attr('id').substring(3); imageV2.requestImgUpdate(trackName,"",""); }); } imageV2.loadRemoteTracks(); makeItemsByDrag.load(); + imageV2.highlightRegion(); } // Drag select in chromIdeogram if($('img#chrom').length == 1) { if($('area.cytoBand').length >= 1) { $('img#chrom').chromDrag(); } } // Track search uses tabs trackSearch.init(); // Drag select initialize if (imageV2.enabled) { // moved from window.load(). dragSelect.load(true);