7a2dbf6721582d73ac9665d9231ffd7fdb703d3a chmalee Wed Jun 3 15:04:56 2026 -0700 Add a link to create a myVariants item to the menu bar under My Data, refs #33808 diff --git src/hg/js/hgTracks.js src/hg/js/hgTracks.js index 19d6115ad8c..15039276089 100644 --- src/hg/js/hgTracks.js +++ src/hg/js/hgTracks.js @@ -1,7456 +1,7479 @@ // hgTracks.js - Javascript for use in hgTracks CGI // Copyright (C) 2008 The Regents of the University of California // "use strict"; // Don't complain about line break before '||' etc: /* jshint -W014 */ /* jshint esversion: 8 */ var debug = false; /* Data passed in from CGI via the hgTracks object: * * string cgiVersion // CGI_VERSION * string chromName // current chromosome * int winStart // genomic start coordinate (0-based, half-open) * int winEnd // genomic end coordinate * int newWinWidth // new width (in bps) if user clicks on the top ruler * boolean revCmplDisp // true if we are in reverse display * int insideX // width of side-bar (in pixels) * int rulerClickHeight // height of ruler (in pixels) - zero if ruler is hidden * boolean inPlaceUpdate // true if in-place-update is turned on * int imgBox* // various drag-scroll values * boolean measureTiming // true if measureTiming is on * Object trackDb // hash of trackDb entries for tracks which are visible on current page * string highlight // highlight string, in format chrom#start#end#color|chrom2#start2#end2#color2|... * string prevHlColor // the last highlight color that the user picked */ /* IE11 compatibility - IE doesn't have string startsWith and never will */ if (!String.prototype.startsWith) { String.prototype.startsWith = function(searchString, position) { position = position || 0; return this.indexOf(searchString, position) === position; }; } function initVars() { // There are various entry points, so we call initVars in several places to make sure all is well if (typeof(hgTracks) !== "undefined" && !genomePos.original) { // remember initial position and size so we can restore it if user cancels genomePos.original = genomePos.getOriginalPos(); genomePos.originalSize = $('#size').text().replace(/,/g, ""); // strip out any commas dragSelect.originalCursor = jQuery('body').css('cursor'); if (typeof (igv) !== "undefined") { igv.initIgvUcsc(); } imageV2.imgTbl = $('#imgTbl'); // imageV2.enabled === true unless: advancedJavascript===false, or trackSearch, or config pg imageV2.enabled = (imageV2.imgTbl && imageV2.imgTbl.length > 0); // jQuery load function with stuff to support drag selection in track img if (theClient.isSafari()) { // Safari has the following bug: if we update the hgTracks map dynamically, // the browser ignores the changes (even though if you look in the DOM the changes // are there). So we have to do a full form submission when the user changes // visibility settings or track configuration. // As of 5.0.4 (7533.20.27) this is problem still exists in safari. // As of 5.1 (7534.50) this problem appears to have been fixed - unfortunately, // logs for 7/2011 show vast majority of safari users are pre-5.1 (5.0.5 is by far // the most common). // // Early versions of Chrome had this problem too, but this problem went away // as of Chrome 5.0.335.1 (or possibly earlier). // // KRR/JAT 2/2016: // This Safari issue is likely resolved in all current versions. However the test // for version had been failing, likely for some time now. // (As of 9.0.9, possibly earlier, the 3rd part of the version is included in the // user agent string, so must be accounted for in string match) // Consequences were that page refresh was used instead of img update (e.g. // for drag-zoom). And UI dialog was unable to update (e.g. via Apply button). imageV2.mapIsUpdateable = false; var reg = new RegExp("Version\/([0-9]+.[0-9]+)(.[0-9]+)? Safari"); var a = reg.exec(navigator.userAgent); if (a && a[1]) { var version = Number(a[1]); if (version >= 5.1) { imageV2.mapIsUpdateable = true; } } } imageV2.inPlaceUpdate = hgTracks.inPlaceUpdate && imageV2.mapIsUpdateable; } } ///////////////////////////////////// ////////// Genomic position ///////// ///////////////////////////////////// var genomePos = { original: null, originalSize: 0, linkFixup: function (pos, id, reg, endParamName) { // fixup external links (e.g. ensembl) var ele = $(document.getElementById(id)); if (ele.length) { var link = ele.attr('href'); var a = reg.exec(link); if (a && a[1]) { ele.attr('href', a[1] + pos.start + "&" + endParamName + "=" + pos.end); } } }, setByCoordinates: function (chrom, start, end) { var newPosition = chrom + ":" + start + "-" + end; genomePos.set(newPosition, end - start + 1); return newPosition; }, getElement: function () { // Return position box object var tags = document.getElementsByName("position"); // There are multiple tags with name === "position" (the visible position text input // and a hidden with id='positionHidden'); we return value of visible element. for (var i = 0; i < tags.length; i++) { var ele = tags[i]; if (ele.id !== "positionHidden") { return ele; } } return null; }, get: function () { // Return current value of position box var ele = genomePos.getElement(); if (ele) { return ele.value; } return null; }, getOriginalPos: function () { return genomePos.original || genomePos.get(); }, revertToOriginalPos: function () { // undo changes to position (i.e. after user aborts a drag-and-select). this.set(this.original, this.originalSize); }, undisguisePosition: function(position) // UN-DISGUISE VMODE { // find the virt position // position should be real chrom span var pos = parsePosition(position); if (!pos) return position; // some parsing error, return original var start = pos.start - 1; var end = pos.end; var chromName = hgTracks.windows[0].chromName; if (pos.chrom !== chromName) return position; // return original var newStart = -1; var newEnd = -1; var lastW = null; var windows = null; for (j=0; j < 3; ++j) { if (j === 0) windows = hgTracks.windowsBefore; if (j === 1) windows = hgTracks.windows; if (j === 2) windows = hgTracks.windowsAfter; for (i=0,len=windows.length; i < len; ++i) { var w = windows[i]; // double check chrom is same thoughout all windows, otherwise warning, return original value if (w.chromName != chromName) { return position; // return original } // check that the regions are ascending and non-overlapping if (lastW && w.winStart < lastW.winEnd) { return position; // return original } // overlap with position? // if intersection, if (w.winEnd > start && end > w.winStart) { var s = Math.max(start, w.winStart); var e = Math.min(end, w.winEnd); var cs = s - w.winStart + w.virtStart; var ce = e - w.winStart + w.virtStart; if (newStart === -1) newStart = cs; newEnd = ce; } lastW = w; } } // return new virt undisguised position as a string var newPos = "multi:" + (newStart+1) + "-" + newEnd; return newPos; }, disguiseSize: function(position) // DISGUISE VMODE { // find the real size of the windows spanned // position should be a real chrom span var pos = parsePosition(position); if (!pos) return 0; var start = pos.start - 1; var end = pos.end; var newSize = 0; var windows = null; for (j=0; j < 3; ++j) { if (j === 0) windows = hgTracks.windowsBefore; if (j === 1) windows = hgTracks.windows; if (j === 2) windows = hgTracks.windowsAfter; for (i=0,len=windows.length; i < len; ++i) { var w = windows[i]; // overlap with position? // if intersection, if (w.winEnd > start && end > w.winStart) { var s = Math.max(start, w.winStart); var e = Math.min(end, w.winEnd); newSize += (e - s); } } } // return real size of the disguised position return newSize; }, disguisePosition: function(position) // DISGUISE VMODE { // find the single-chrom range spanned // position should be virt var pos = parsePosition(position); if (!pos) return position; // some parsing error, return original var start = pos.start - 1; var end = pos.end; var chromName = hgTracks.windows[0].chromName; var newStart = -1; var newEnd = -1; var lastW = null; var windows = null; for (j=0; j < 3; ++j) { if (j === 0) windows = hgTracks.windowsBefore; if (j === 1) windows = hgTracks.windows; if (j === 2) windows = hgTracks.windowsAfter; for (i=0,len=windows.length; i < len; ++i) { var w = windows[i]; // double check chrom is same thoughout all windows, otherwise warning, return original value if (w.chromName != chromName) { return position; // return undisguised original } // check that the regions are ascending and non-overlapping if (lastW && w.winStart < lastW.winEnd) { return position; // return undisguised original } // overlap with position? // if intersection, if (w.virtEnd > start && end > w.virtStart) { var s = Math.max(start, w.virtStart); var e = Math.min(end, w.virtEnd); var cs = s - w.virtStart + w.winStart; var ce = e - w.virtStart + w.winStart; if (newStart === -1) newStart = cs; newEnd = ce; } lastW = w; } } // return new non-virt disguised position as a string var newPos = chromName + ":" + (newStart+1) + "-" + newEnd; return newPos; }, set: function (position, size) { // Set value of position and size (in hiddens and input elements). // We assume size has already been commified. // Either position or size may be null. // stack dump // DEBUG //console.trace(); // NOT work on safari //var obj = {}; //Error.captureStackTrace(obj); //warn("genomePos.set() called "+obj.stack); position = position.replace(/,/g, ""); // strip out any commas position.replace("virt:", "multi:"); if (position) { // DISGUISE VMODE //warn("genomePos.set() called, position = "+position); if (hgTracks.virtualSingleChrom && (position.search("multi:")===0)) { var newPosition = genomePos.disguisePosition(position); //warn("genomePos.set() position = "+position+", newPosition = "+newPosition); position = newPosition; } } if (position) { // There are multiple tags with name === "position" // (one in TrackHeaderForm and another in TrackForm). var tags = document.getElementsByName("position"); for (var i = 0; i < tags.length; i++) { var ele = tags[i]; ele.value = position; } } var pos = parsePosition(position); if ($('#positionDisplay').length) { // add commas to positionDisplay var commaPosition = position; if (pos) commaPosition = pos.chrom+":"+commify(pos.start)+"-"+commify(pos.end); $('#positionDisplay').text(commaPosition); } if (size) { if (hgTracks.virtualSingleChrom && (position.search("multi:")!==0)) { var newSize = genomePos.disguiseSize(position); //warn("genomePos.set() position = "+position+", newSize = "+newSize); if (newSize > 0) size = newSize; } $('#size').text(commify(size)); // add commas } if (pos) { // fixup external static links on page' // Example ensembl link: // http://www.ensembl.org/Homo_sapiens/contigview?chr=21&start=33031934&end=33041241 genomePos.linkFixup(pos, "ensemblLink", new RegExp("(.+start=)[0-9]+"), "end"); // Example NCBI Map Viewer link (obsolete): // https://www.ncbi.nlm.nih.gov/mapview/maps.cgi?taxid=9606&CHR=21&BEG=33031934&END=33041241 genomePos.linkFixup(pos, "ncbiLink", new RegExp("(.+BEG=)[0-9]+"), "END"); // Example NCBI Genome Data Viewer link // https://www.ncbi.nlm.nih.gov/genome/gdv/browser/?id=GCF_000001405.37&chr=4&from=45985744&to=45991655&context=genome genomePos.linkFixup(pos, "ncbiLink", new RegExp("(.+from=)[0-9]+"), "to"); // Example medaka link: // http://utgenome.org/medakabrowser_ens_jump.php?revision=version1.0&chr=chromosome18&start=14435198&end=14444829 genomePos.linkFixup(pos, "medakaLink", new RegExp("(.+start=)[0-9]+"), "end"); var link; var reg; var a; if ($('#wormbaseLink').length) { // e.g. http://www.wormbase.org/db/gb2/gbrowse/c_elegans?name=II:14646301-14667800 link = $('#wormbaseLink').attr('href'); reg = new RegExp("(.+:)[0-9]+"); a = reg.exec(link); if (a && a[1]) { $('#wormbaseLink').attr('href', a[1] + pos.start + "-" + pos.end); } } // Fixup DNA link; e.g.: // hgc?hgsid=2999470&o=114385768&g=getDna&i=mixed&c=chr7&l=114385768&r=114651696&db=panTro2&hgsid=2999470 if ($('#dnaLink').length) { link = $('#dnaLink').attr('href'); reg = new RegExp("(.+&o=)[0-9]+.+&db=[^&]+(.*)"); a = reg.exec(link); if (a && a[1]) { var url = a[1] + (pos.start - 1) + "&g=getDna&i=mixed&c=" + pos.chrom; url += "&l=" + (pos.start - 1) + "&r=" + pos.end + "&db=" + getDb() + a[2]; $('#dnaLink').attr('href', url); } } } if (!imageV2.backSupport) imageV2.markAsDirtyPage(); }, getXLimits : function(img, slop) { // calculate the min/max x position for drag-select, such that user cannot drag into the label area var imgWidth = jQuery(img).width(); var imgOfs = jQuery(img).offset(); var leftX = hgTracks.revCmplDisp ? imgOfs.left - slop : imgOfs.left + hgTracks.insideX - slop; var rightX = hgTracks.revCmplDisp ? imgOfs.left + imgWidth - hgTracks.insideX + slop : imgOfs.left + imgWidth + slop; return [leftX, rightX]; }, check: function (img, selection) { // return true if user's selection is still w/n the img (including some slop). var imgWidth = jQuery(img).width(); var imgHeight = jQuery(img).height(); var imgOfs = jQuery(img).offset(); var slop = 10; // No need to check the x limits anymore, as imgAreaSelect is doing that now. return ( (selection.event.pageY >= (imgOfs.top - slop)) && (selection.event.pageY < (imgOfs.top + imgHeight + slop))); }, pixelsToBases: function (img, selStart, selEnd, winStart, winEnd, addHalfBp) { // Convert image coordinates to chromosome coordinates var imgWidth = jQuery(img).width() - hgTracks.insideX; var width = hgTracks.winEnd - hgTracks.winStart; var mult = width / imgWidth; // mult is bp/pixel multiplier // where does a bp position start on the screen? // For things like drag-select, if the user ends just before the nucleotide itself, do not count // the nucleotide itself as selected. But for things like clicks onto // a selection, if the user right-clicks just before the middle of the // nucleotide, we certainly want to use this position. var halfBpWidth = 0; if (addHalfBp) halfBpWidth = (imgWidth / width) / 2; // how many pixels does one bp take up; var startDelta; // startDelta is how many bp's to the right/left var x1; // The magic number three appear at another place in the code // as LEFTADD. It was originally annotated as "borders or cgi item calc // ?" by Larry. It has to be used when going any time when converting // between pixels and coordinates. selStart -= 3; selEnd -= 3; if (hgTracks.revCmplDisp) { x1 = Math.min(imgWidth, selStart); startDelta = Math.floor(mult * (imgWidth - x1 - halfBpWidth)); } else { x1 = Math.max(hgTracks.insideX, selStart); startDelta = Math.floor(mult * (x1 - hgTracks.insideX + halfBpWidth)); } var endDelta; var x2; if (hgTracks.revCmplDisp) { endDelta = startDelta; x2 = Math.min(imgWidth, selEnd); startDelta = Math.floor(mult * (imgWidth - x2 + halfBpWidth)); } else { x2 = Math.max(hgTracks.insideX, selEnd); endDelta = Math.floor(mult * (x2 - hgTracks.insideX - halfBpWidth)); } var newStart = hgTracks.winStart + startDelta; var newEnd = hgTracks.winStart + 1 + endDelta; // if user selects space between two bases, start>end can happen if (newStart >= newEnd) newStart = newEnd-1; if (newEnd > winEnd) { newEnd = winEnd; } return {chromStart : newStart, chromEnd : newEnd}; }, chromToVirtChrom: function (chrom, chromStart, chromEnd) { // Convert regular chromosome position to virtual chrom coordinates using hgTracks.windows list // Consider the first contiguous set of overlapping regions to define the match (for now). // only works for regions covered by the current hgTracks.windows var virtStart = -1, virtEnd = -1; var s,e; var i, len; for (i = 0, len = hgTracks.windows.length; i < len; ++i) { var w = hgTracks.windows[i]; var overlap = (chrom == w.chromName && chromEnd > w.winStart && w.winEnd > chromStart); if (virtStart == -1) { if (overlap) { // when they overlap the first time s = Math.max(chromStart, w.winStart); e = Math.min(chromEnd, w.winEnd); virtStart = w.virtStart + (s - w.winStart); virtEnd = w.virtStart + (e - w.winStart); } else { // until they overlap // do nothing } } else { if (overlap) { // while they continue to overlap, extend e = Math.min(chromEnd, w.winEnd); virtEnd = w.virtStart + (e - w.winStart); } else { // when they do not overlap anymore, stop break; } } } return {chromStart : virtStart, chromEnd : virtEnd}; }, selectionPixelsToBases: function (img, selection) { // Convert selection x1/x2 coordinates to chromStart/chromEnd. return genomePos.pixelsToBases(img, selection.x1, selection.x2, hgTracks.winStart, hgTracks.winEnd, true); }, update: function (img, selection, singleClick) { var pos = genomePos.pixelsToBases(img, selection.x1, selection.x2, hgTracks.winStart, hgTracks.winEnd, true); // singleClick is true when the mouse hasn't moved (or has only moved a small amount). if (singleClick) { var center = (pos.chromStart + pos.chromEnd)/2; pos.chromStart = Math.floor(center - hgTracks.newWinWidth/2); pos.chromEnd = pos.chromStart + hgTracks.newWinWidth; // clip if (pos.chromStart < hgTracks.chromStart) pos.chromStart = hgTracks.chromStart; // usually 1 if (pos.chromEnd > hgTracks.chromEnd) pos.chromEnd = hgTracks.chromEnd; // usually virt chrom size // save current position so that that it may be restored after highlight or cancel. genomePos.original = genomePos.getOriginalPos(); genomePos.originalSize = $('#size').text().replace(/,/g, ""); // strip out any commas } var newPosition = genomePos.setByCoordinates(hgTracks.chromName, pos.chromStart+1, pos.chromEnd); return newPosition; }, handleChange: function (response, status) { var json = JSON.parse(response); genomePos.set(json.pos); }, changeAssemblies: function (ele) // UNUSED? Larry's experimental code { // code to update page when user changes assembly select list. $.ajax({ type: "GET", url: "../cgi-bin/hgApi", data: cart.varsToUrlData({ 'hgsid': getHgsid(), 'cmd': 'defaultPos', 'db': getDb() }), dataType: "html", trueSuccess: genomePos.handleChange, success: catchErrorOrDispatch, error: errorHandler, cache: true }); return false; }, convertedVirtCoords : {chromStart : -1, chromEnd : -1}, handleConvertChromPosToVirtCoords: function (response, status) { var virtStart = -1, virtEnd = -1; var newJson = scrapeVariable(response, "convertChromToVirtChrom"); if (!newJson) { warn("convertChromToVirtChrom object is missing from the response"); } else { virtStart = newJson.virtWinStart; virtEnd = newJson.virtWinEnd; } genomePos.convertedVirtCoords = {chromStart : virtStart, chromEnd : virtEnd}; }, convertChromPosToVirtCoords: function (chrom, chromStart, chromEnd) { // code to convert chrom position to virt coords genomePos.convertedVirtCoords = {chromStart : -1, chromEnd : -1}; // reset var pos = chrom+":"+(chromStart+1)+"-"+chromEnd; // easier to pass 1 parameter than 3 $.ajax({ type: "GET", async: false, // wait for result url: "../cgi-bin/hgTracks", data: cart.varsToUrlData({ 'hgt.convertChromToVirtChrom': pos, 'hgt.trackImgOnly' : 1, 'hgsid': getHgsid(), 'db': getDb() }), dataType: "html", trueSuccess: genomePos.handleConvertChromPosToVirtCoords, success: catchErrorOrDispatch, error: errorHandler, cache: false }); return genomePos.convertedVirtCoords; }, positionDisplayDialog: function () // Show the virtual and real positions of the windows { var position = genomePos.get(); position.replace("virt:", "multi:"); var positionDialog = $("#positionDialog")[0]; if (!positionDialog) { $("body").append("
"); positionDialog = $("#positionDialog")[0]; } if (hgTracks.windows) { var i, len, end; var matches = /^multi:[0-9]+-([0-9]+)/.exec(position); var modeType = (hgTracks.virtModeType === "customUrl" ? "Custom regions on virtual chromosome" : (hgTracks.virtModeType === "exonMostly" ? "Exon view of" : (hgTracks.virtModeType === "geneMostly" ? "Gene view of" : (hgTracks.virtModeType === "singleAltHaplo" ? "Alternate haplotype as virtual chromosome" : "Unknown mode")))); var str = modeType + " " + position; if (matches) { end = matches[1]; if (end < hgTracks.chromEnd) { str += ". Full virtual region is multi:1-" + hgTracks.chromEnd + ". Zoom out to view."; } } if (!(hgTracks.virtualSingleChrom && (hgTracks.windows.length === 1))) { var w; if (hgTracks.windows.length <= 10) { str += "

\n"; for (i=0,len=hgTracks.windows.length; i < len; ++i) { w = hgTracks.windows[i]; str += "\n"; } str += "
" + w.chromName + ":" + (w.winStart+1) + "-" + w.winEnd + "" + (w.winEnd - w.winStart) + " bp" + "

\n"; } else { str += "
\n"; } } $("#positionDisplayPosition").html(str); } else { $("#positionDisplayPosition").html(position); } $(positionDialog).dialog({ modal: true, title: "Multi-Region Position Ranges", closeOnEscape: true, resizable: false, autoOpen: false, minWidth: 400, minHeight: 40, close: function() { // All exits to dialog should go through this $(imageV2.imgTbl).imgAreaSelect({hide:true}); $(this).hide(); $('body').css('cursor', ''); // Occasionally wait cursor got left behind } }); $(positionDialog).dialog('open'); } }; ///////////////////////////////////// //// Creating items ///// ///////////////////////////////////// var myVariants = { createBedForm: function(dialogEle) { // Shared top fields (visible in all manual modes) const commonTopFields = [ { label: "Label", id: "name", type: "text", placeholder: "Optional item label", info: "A short label for this annotation, displayed in the browser" }, { label: "Color", id: "color", type: "text" }, ]; // SNV-only field (between commonTop and alt/sequence) const snvOnlyFields = [ { label: "Ref", id: "ref", type: "text", placeholder: "Optional reference allele sequence", info: "Reference allele sequence at this position" }, ]; // Alt/Sequence field: shown for SNV (label="Alt") and CNV (label="Sequence") const altField = { label: "Alt", id: "alt", type: "text", placeholder: "Optional alternate allele sequence", info: "Alternate (variant) allele sequence" }; // Position fields: shown always for Transcript; collapsible for SNV/CNV const positionFields = [ { label: "Chromosome", id: "chrom", type: "text" }, { label: "Start", id: "start", type: "number", info: "1-based start position on the chromosome" }, { label: "End", id: "end", type: "number", info: "1-based end position on the chromosome (inclusive)" }, { label: "Score", id: "score", type: "number", info: "Score from 0-1000. Higher scores display darker" }, { label: "Strand", id: "strand", type: "text", info: "Strand: + for forward, - for reverse, . for unknown" }, ]; // Transcript-only CDS fields (displayed as CDS Start/End; stored as thickStart/thickEnd) const cdsFields = [ { label: "CDS Start", id: "thickStart", type: "number", info: "Start of the coding region" }, { label: "CDS End", id: "thickEnd", type: "number", info: "End of the coding region" }, ]; // Shared bottom fields const commonBottomFields = [ { label: "Mouseover", id: "mouseover", type: "text", placeholder: "Short text shown on hover", info: "Short text shown when hovering over this item. If empty, the label and alleles are displayed" }, { label: "Description", id: "description", type: "textarea", placeholder: "Longer description/notes", info: "Longer notes or comments about this annotation. Displayed on the details page" }, { label: "Project", id: "project", type: "text", placeholder: "Optional project name", info: "Group annotations by project. Projects with no annotations are automatically removed from the list" }, ]; // CNV vocabulary, mirrored on the server in myVariantsTrack.c:validCnvTypes const cnvTypeOptions = ["deletion", "duplication", "insertion", "inversion", "translocation", "complex", "breakend"]; const form = document.createElement("form"); form.className = "myVariants-form"; form.action = "hgTracks"; form.method = "post"; form.id = "myVariants-form"; // First: HGVS/position input box (primary interaction method) let quickInpDiv = document.createElement("div"); quickInpDiv.id = "hgvsInputDiv"; form.appendChild(quickInpDiv); let quickInp = document.createElement("input"); let quickInpLabel = document.createElement("label"); quickInp.type = "text"; quickInp.id = "hgvsInput"; quickInp.name = "myVariantsHgvsInput"; quickInp.style.width = "300px"; quickInpLabel.textContent = "Enter HGVS, position, or gene symbol"; quickInpLabel.style.display = "inline-block"; quickInpLabel.style.minWidth = "200px"; quickInpLabel.style.marginRight = "8px"; quickInpLabel.for = "hgvsInput"; let resizeDialog = function() { var dialog = $("#myVariantsDialog"); var hgvsVisible = document.getElementById("hgvsInputDiv").style.display !== "none"; if (hgvsVisible) { dialog.dialog("option", "width", 580); dialog.dialog("option", "height", "auto"); dialog.dialog("option", "position", { my: "center", at: "center", of: window }); } else { dialog.dialog("option", "width", Math.min(1000, window.innerWidth * 0.9)); dialog.dialog("option", "height", Math.min(800, window.innerHeight * 0.9)); dialog.dialog("option", "position", { my: "top+30", at: "top", of: window }); } }; let updatePositionSummary = function() { let summaryText = document.getElementById("positionSummaryText"); if (!summaryText) return; let chrom = document.getElementById("chrom"); let start = document.getElementById("start"); let end = document.getElementById("end"); if (chrom && start && end) { let startVal = parseInt(start.value); let endVal = parseInt(end.value); let startFmt = isNaN(startVal) ? start.value : startVal.toLocaleString(); let endFmt = isNaN(endVal) ? end.value : endVal.toLocaleString(); summaryText.textContent = chrom.value + ":" + startFmt + "-" + endFmt; } }; // Switch the form into one of four modes and remember the choice so the // next dialog open restarts in the same place. let applyMode = function(mode) { const isHgvs = mode === "hgvs"; const isTranscript = mode === "transcript"; const isSnv = mode === "snv"; const isCnv = mode === "cnv"; document.getElementById("hgvsInputDiv").style.display = isHgvs ? "" : "none"; document.getElementById("manualInputDiv").style.display = isHgvs ? "none" : ""; document.getElementById("hgvsManualToggle").textContent = isHgvs ? "Or edit item fields manually" : "Back to quick input mode"; if (!isHgvs) { document.getElementById("typeTranscript").checked = isTranscript; document.getElementById("typeSnv").checked = isSnv; document.getElementById("typeCnv").checked = isCnv; } document.getElementById("refWrapper").style.display = isSnv ? "" : "none"; document.getElementById("altWrapper").style.display = (isSnv || isCnv) ? "" : "none"; document.getElementById("altLabel").textContent = isCnv ? "Sequence" : "Alt"; document.getElementById("cnvTypeWrapper").style.display = isCnv ? "" : "none"; if (isTranscript) { advancedDiv.style.display = "none"; cdsBlocksDiv.style.display = "none"; posWraps.forEach(function(w) { w.style.display = ""; }); cdsWraps.forEach(function(w) { w.style.display = ""; }); blocksSection.style.display = ""; const order = [ typeRadioBar, posSummary, posWraps[0], posWraps[1], posWraps[2], nameWrap, posWraps[3], posWraps[4], cdsWraps[0], cdsWraps[1], colorWrap, mouseoverWrap, descriptionWrap, projectWrap, blocksSection, advancedDiv, cdsBlocksDiv, refWrapper, cnvTypeWrapper, altWrapper, customFieldsSection ]; order.forEach(function(el) { manualInpDiv.appendChild(el); }); } else { // Pack wrappers back into their collapsible containers. posWraps.forEach(function(w) { advancedDiv.appendChild(w); }); cdsWraps.forEach(function(w) { cdsBlocksDiv.appendChild(w); }); cdsBlocksDiv.appendChild(blocksSection); advancedDiv.style.display = "none"; cdsBlocksDiv.style.display = "none"; const order = [ typeRadioBar, posSummary, advancedDiv, nameWrap, colorWrap, refWrapper, cnvTypeWrapper, altWrapper, cdsBlocksDiv, mouseoverWrap, descriptionWrap, projectWrap, customFieldsSection ]; order.forEach(function(el) { manualInpDiv.appendChild(el); }); } document.getElementById("positionSummaryDiv").style.display = isTranscript ? "none" : ""; if (posEditLink) posEditLink.textContent = "[edit]"; try { localStorage.setItem("myVariantsLastMode", mode); } catch (e) { /* private mode */ } resizeDialog(); }; let currentType = function() { if (document.getElementById("typeTranscript").checked) return "transcript"; if (document.getElementById("typeCnv").checked) return "cnv"; return "snv"; }; let toggleForm = function(event) { event.preventDefault(); const inHgvs = document.getElementById("hgvsInputDiv").style.display !== "none"; applyMode(inHgvs ? currentType() : "hgvs"); }; let toggleContainer = document.createElement("p"); let toggle = document.createElement("a"); toggle.href = "#"; toggle.id = "hgvsManualToggle"; toggle.addEventListener("click", toggleForm); toggle.textContent = "Or edit item fields manually"; toggleContainer.appendChild(toggle); quickInpDiv.appendChild(quickInpLabel); quickInpDiv.appendChild(quickInp); form.appendChild(toggleContainer); // Manual input div (contains simple + advanced fields) let manualInpDiv = document.createElement("div"); manualInpDiv.id = "manualInputDiv"; manualInpDiv.style.display = "none"; let posSummary = document.createElement("div"); posSummary.id = "positionSummaryDiv"; posSummary.style.cssText = "margin-bottom:12px; padding:6px 10px; background:#f0f4f8; border:1px solid #d0d7de; border-radius:4px; font-size:13px;"; let posSummaryLabel = document.createElement("b"); posSummaryLabel.textContent = "Position: "; posSummary.appendChild(posSummaryLabel); let posSummaryText = document.createElement("span"); posSummaryText.id = "positionSummaryText"; posSummary.appendChild(posSummaryText); let posSummarySuffix = document.createElement("span"); posSummarySuffix.style.color = "#888"; posSummarySuffix.textContent = " (from current view)"; posSummary.appendChild(posSummarySuffix); let posEditLink = document.createElement("a"); posEditLink.href = "#"; posEditLink.textContent = "[edit]"; posEditLink.style.cssText = "margin-left:8px; font-size:12px;"; posEditLink.addEventListener("click", function(event) { event.preventDefault(); let advDiv = document.getElementById("advancedFieldsDiv"); if (!advDiv) return; if (advDiv.style.display === "none") { advDiv.style.display = ""; posEditLink.textContent = "[hide]"; let chromField = document.getElementById("chrom"); if (chromField) { chromField.scrollIntoView({behavior: "smooth", block: "nearest"}); chromField.focus(); } } else { advDiv.style.display = "none"; posEditLink.textContent = "[edit]"; } }); posSummary.appendChild(posEditLink); manualInpDiv.appendChild(posSummary); // Helper function to create form field (uses createInfoIcon from utils.js) let createField = function(field, container) { const wrapper = document.createElement("div"); wrapper.style.marginBottom = "8px"; const label = document.createElement("label"); label.htmlFor = field.id; label.textContent = field.label; label.style.display = "inline-block"; label.style.minWidth = "140px"; let input; if (field.type === "textarea") { input = document.createElement("textarea"); input.rows = "4"; input.cols = "60"; } else { input = document.createElement("input"); input.type = field.type; } input.id = field.id; if (field.id === "chrom") { input.value = hgTracks.chromName; } if (field.type === "number") { input.min = 0; if (field.id === "start" || field.id === "thickStart") { input.value = hgTracks.winStart; } if (field.id === "end" || field.id === "thickEnd") { input.value = hgTracks.winEnd; } if (field.id === "score") { input.value = 0; input.max = 1000; } } if (field.id === "strand") { input.value = "."; } if (field.id === "color") { input.value = "#000000"; input.style.width = "70px"; } if (field.placeholder) { input.placeholder = field.placeholder; } wrapper.appendChild(label); wrapper.appendChild(input); if (field.info) { wrapper.appendChild(createInfoIcon(field.info)); } container.appendChild(wrapper); return input; }; // Helper function to create project field with dropdown if projects exist let createProjectField = function(container) { const wrapper = document.createElement("div"); wrapper.style.marginBottom = "8px"; const label = document.createElement("label"); label.htmlFor = "project"; label.textContent = "Project"; label.style.display = "inline-block"; label.style.minWidth = "140px"; wrapper.appendChild(label); // Check if we have existing projects from the server let existingProjects = (typeof hgTracks !== 'undefined' && hgTracks.myVariantsProjects) ? hgTracks.myVariantsProjects : []; if (existingProjects.length > 0) { // Create dropdown with existing projects let select = document.createElement("select"); select.id = "projectSelect"; select.style.marginRight = "8px"; // Add empty option let emptyOpt = document.createElement("option"); emptyOpt.value = ""; emptyOpt.textContent = "(none)"; select.appendChild(emptyOpt); // Add existing projects existingProjects.forEach(proj => { let opt = document.createElement("option"); opt.value = proj; opt.textContent = proj; select.appendChild(opt); }); // Add "Add new..." option let newOpt = document.createElement("option"); newOpt.value = "__new__"; newOpt.textContent = "Add new..."; select.appendChild(newOpt); wrapper.appendChild(select); // Hidden text input for new project (shown when "Add new..." selected) let newProjectInput = document.createElement("input"); newProjectInput.type = "text"; newProjectInput.id = "project"; newProjectInput.placeholder = "Enter new project name"; newProjectInput.style.display = "none"; wrapper.appendChild(newProjectInput); // Toggle between dropdown and text input select.addEventListener("change", function() { if (select.value === "__new__") { newProjectInput.style.display = ""; newProjectInput.focus(); } else { newProjectInput.style.display = "none"; newProjectInput.value = select.value; } }); // Keep text input synced with dropdown selection newProjectInput.addEventListener("blur", function() { if (newProjectInput.value === "" && select.value === "__new__") { select.value = ""; newProjectInput.style.display = "none"; } }); } else { // No existing projects - just show text input let input = document.createElement("input"); input.type = "text"; input.id = "project"; input.placeholder = "Optional project name"; wrapper.appendChild(input); } // Add info icon wrapper.appendChild(createInfoIcon("Group annotations by project. Projects with no annotations are automatically removed from the list")); container.appendChild(wrapper); }; // Type radio bar: Transcript | Variant -> Short | CNV. Inserted at the very // top of manualInpDiv (before the existing position summary). let typeRadioBar = document.createElement("div"); typeRadioBar.id = "typeRadioBar"; typeRadioBar.style.cssText = "margin-bottom:12px; padding:8px 10px; background:#f7f7f7; border:1px solid #ddd; border-radius:4px;"; typeRadioBar.innerHTML = "
Annotation type
" + "
" + "" + "" + "" + "
"; manualInpDiv.insertBefore(typeRadioBar, manualInpDiv.firstChild); let onTypeRadioChange = function() { applyMode(currentType()); }; // The form is not yet in the document, so query the local subtree. typeRadioBar.querySelectorAll("input[type=radio]").forEach(function(r) { r.addEventListener("change", onTypeRadioChange); }); let colorInput = null; let nameWrap, colorWrap; commonTopFields.forEach(function(field) { let input = createField(field, manualInpDiv); if (field.id === "color") { colorInput = input; colorWrap = input.parentNode; } else if (field.id === "name") { nameWrap = input.parentNode; } }); // SNV-only ref input let refWrapper = document.createElement("div"); refWrapper.id = "refWrapper"; createField(snvOnlyFields[0], refWrapper); manualInpDiv.appendChild(refWrapper); // CNV-only type select let cnvTypeWrapper = document.createElement("div"); cnvTypeWrapper.id = "cnvTypeWrapper"; cnvTypeWrapper.style.marginBottom = "8px"; let cnvTypeLabel = document.createElement("label"); cnvTypeLabel.htmlFor = "cnvType"; cnvTypeLabel.textContent = "CNV type"; cnvTypeLabel.style.cssText = "display:inline-block; min-width:140px;"; let cnvTypeSelect = document.createElement("select"); cnvTypeSelect.id = "cnvType"; cnvTypeOptions.forEach(function(t) { let opt = document.createElement("option"); opt.value = t; opt.textContent = t; cnvTypeSelect.appendChild(opt); }); cnvTypeWrapper.appendChild(cnvTypeLabel); cnvTypeWrapper.appendChild(cnvTypeSelect); cnvTypeWrapper.appendChild(createInfoIcon("CNV vocabulary follows gnomAD")); manualInpDiv.appendChild(cnvTypeWrapper); // Alt / Sequence input (shared between SNV and CNV; label switches in applyMode) let altWrapper = document.createElement("div"); altWrapper.id = "altWrapper"; altWrapper.style.marginBottom = "8px"; let altLabel = document.createElement("label"); altLabel.htmlFor = "alt"; altLabel.id = "altLabel"; altLabel.textContent = "Alt"; altLabel.style.cssText = "display:inline-block; min-width:140px;"; let altInput = document.createElement("input"); altInput.type = "text"; altInput.id = "alt"; altInput.placeholder = altField.placeholder; altWrapper.appendChild(altLabel); altWrapper.appendChild(altInput); altWrapper.appendChild(createInfoIcon(altField.info)); manualInpDiv.appendChild(altWrapper); let advancedDiv = document.createElement("div"); advancedDiv.id = "advancedFieldsDiv"; advancedDiv.style.display = "none"; let posWraps = positionFields.map(function(field) { return createField(field, advancedDiv).parentNode; }); manualInpDiv.appendChild(advancedDiv); let cdsBlocksDiv = document.createElement("div"); cdsBlocksDiv.id = "cdsBlocksDiv"; cdsBlocksDiv.style.display = "none"; let cdsWraps = cdsFields.map(function(field) { return createField(field, cdsBlocksDiv).parentNode; }); let blocksSection = document.createElement("div"); blocksSection.id = "blocksSection"; blocksSection.style.cssText = "margin-top:12px; padding-top:10px; border-top:1px solid #ddd;"; let blocksLabel = document.createElement("div"); blocksLabel.style.cssText = "font-weight:bold; margin-bottom:8px; font-size:13px;"; blocksLabel.textContent = "Blocks (optional)"; blocksSection.appendChild(blocksLabel); let blocksHint = document.createElement("div"); blocksHint.style.cssText = "font-size:12px; color:#666; margin-bottom:6px;"; blocksHint.textContent = "Offsets are relative to Start. First offset must be 0; " + "last block must reach End. Leave empty for a single full-span block."; blocksSection.appendChild(blocksHint); let blocksContainer = document.createElement("div"); blocksContainer.id = "blocksContainer"; blocksSection.appendChild(blocksContainer); let hCount = document.createElement("input"); hCount.type = "hidden"; hCount.id = "blockCount"; let hSizes = document.createElement("input"); hSizes.type = "hidden"; hSizes.id = "blockSizes"; let hStarts = document.createElement("input"); hStarts.type = "hidden"; hStarts.id = "chromStarts"; blocksSection.appendChild(hCount); blocksSection.appendChild(hSizes); blocksSection.appendChild(hStarts); cdsBlocksDiv.appendChild(blocksSection); manualInpDiv.appendChild(cdsBlocksDiv); let mouseoverWrap, descriptionWrap, projectWrap; commonBottomFields.forEach(function(field) { if (field.id === "project") { createProjectField(manualInpDiv); projectWrap = manualInpDiv.lastElementChild; } else { let input = createField(field, manualInpDiv); if (field.id === "mouseover") mouseoverWrap = input.parentNode; else if (field.id === "description") descriptionWrap = input.parentNode; } }); let customFieldsSection = document.createElement("div"); customFieldsSection.id = "customFieldsSection"; customFieldsSection.style.cssText = "margin-top:12px; padding-top:10px; border-top:1px solid #ddd;"; let customFieldsLabel = document.createElement("div"); customFieldsLabel.style.cssText = "font-weight:bold; margin-bottom:8px; font-size:13px;"; customFieldsLabel.textContent = "Custom Fields"; customFieldsSection.appendChild(customFieldsLabel); let customFieldsList = document.createElement("div"); customFieldsList.id = "customFieldsList"; customFieldsSection.appendChild(customFieldsList); let reservedNames = ["bin", "chrom", "chromStart", "chromEnd", "name", "score", "strand", "thickStart", "thickEnd", "itemRgb", "blockCount", "blockSizes", "chromStarts", "description", "db", "ref", "alt", "project", "mouseover", "itemType", "cnvType", "id"]; let validateFieldName = function(nameInput) { let name = nameInput.value.trim(); if (!name) return; // empty is ok, will be skipped on submit let valid = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name); if (!valid) { alert("Invalid field name: '" + name + "'. Must start with a letter or underscore, " + "and contain only letters, numbers, and underscores."); nameInput.focus(); return false; } if (name.startsWith("_hidden_")) { alert("Field names cannot start with '_hidden_'."); nameInput.focus(); return false; } if (reservedNames.indexOf(name) >= 0) { alert("'" + name + "' is a reserved field name."); nameInput.focus(); return false; } // Check for duplicates among all custom field name inputs let allNames = document.querySelectorAll("#customFieldsList .customFieldName"); let count = 0; allNames.forEach(function(inp) { if (inp.value.trim() === name) count++; }); if (count > 1) { alert("Duplicate field name: '" + name + "'."); nameInput.focus(); return false; } return true; }; let addCustomFieldRow = function(existingName) { // existingName: if provided, this is a pre-populated existing field (read-only name) let row = document.createElement("div"); row.className = "customFieldRow"; row.style.cssText = "display:flex; align-items:center; gap:6px; margin-bottom:6px;"; if (existingName) row.dataset.existing = "true"; let nameInput = document.createElement("input"); nameInput.type = "text"; nameInput.className = "customFieldName"; nameInput.placeholder = "Field name"; nameInput.style.cssText = "width:120px;"; if (existingName) { nameInput.value = existingName; nameInput.readOnly = true; nameInput.style.cssText = "width:120px; background:#e8e8e8; color:#555;"; } else { // Add blur validation for new field names nameInput.addEventListener("blur", function() { if (nameInput.value.trim()) validateFieldName(nameInput); }); } let valueInput = document.createElement("input"); valueInput.type = "text"; valueInput.className = "customFieldValue"; valueInput.placeholder = "Value (optional)"; valueInput.style.cssText = "width:180px;"; row.appendChild(nameInput); row.appendChild(valueInput); if (existingName) { // Hide button for existing fields (soft-delete via _hidden_ prefix) let hideBtn = document.createElement("button"); hideBtn.type = "button"; hideBtn.textContent = "Hide"; hideBtn.title = "Hide this custom field (data preserved, can be restored later)"; hideBtn.style.cssText = "border:1px solid #ccc; background:#f5f5f5; color:#888; font-size:11px; cursor:pointer; padding:1px 6px; border-radius:3px;"; hideBtn.addEventListener("click", function() { if (!confirm("Hide the '" + existingName + "' field? Data will be preserved but the field will no longer appear.")) return; // Send ALTER TABLE CHANGE COLUMN request to rename with _hidden_ prefix let hideUrl = "../cgi-bin/hgTracks?hgt_doJsCommand=" + encodeURIComponent("myVariants myVariants " + JSON.stringify({hideField: existingName})) + "&hgt.trackImgOnly=1&hgt.ideogramToo=1&hgsid=" + getHgsid() + "&db=" + getDb(); fetch(hideUrl, { method: "POST", credentials: "same-origin" }) .then(function() { row.remove(); }) .catch(function(err) { alert("Error hiding field: " + err.message); }); }); row.appendChild(hideBtn); } else { // Remove button for newly added rows let removeBtn = document.createElement("button"); removeBtn.type = "button"; removeBtn.textContent = "\u00D7"; removeBtn.title = "Remove this field"; removeBtn.style.cssText = "border:none; background:none; color:#c00; font-size:18px; cursor:pointer; padding:0 4px; line-height:1;"; removeBtn.addEventListener("click", function() { row.remove(); }); row.appendChild(removeBtn); } customFieldsList.appendChild(row); if (!existingName) nameInput.focus(); }; // Pre-populate existing custom fields from server let existingCustomFields = (typeof hgTracks !== 'undefined' && hgTracks.myVariantsCustomFields) ? hgTracks.myVariantsCustomFields : []; existingCustomFields.forEach(function(fieldName) { addCustomFieldRow(fieldName); }); let addBtn = document.createElement("button"); addBtn.type = "button"; addBtn.id = "addCustomFieldBtn"; addBtn.textContent = "+ Add Custom Field"; addBtn.style.cssText = "margin-top:4px; padding:3px 10px; font-size:12px; cursor:pointer;"; addBtn.addEventListener("click", function(event) { event.preventDefault(); addCustomFieldRow(); }); customFieldsSection.appendChild(addBtn); // Hidden fields restore UI let hiddenFields = (typeof hgTracks !== 'undefined' && hgTracks.myVariantsHiddenFields) ? hgTracks.myVariantsHiddenFields : []; if (hiddenFields.length > 0) { let hiddenSection = document.createElement("div"); hiddenSection.id = "hiddenFieldsSection"; hiddenSection.style.cssText = "margin-top:8px; padding:4px 8px; background:#faf8f0; border:1px solid #e0dcc8; border-radius:3px; font-size:12px;"; let hiddenLabel = document.createElement("span"); hiddenLabel.style.cssText = "color:#888; margin-right:8px;"; hiddenLabel.textContent = "Hidden fields:"; hiddenSection.appendChild(hiddenLabel); hiddenFields.forEach(function(fieldName) { let chip = document.createElement("span"); chip.style.cssText = "display:inline-block; margin:2px 4px; padding:2px 6px; background:#e8e8e8; border-radius:3px;"; chip.textContent = fieldName + " "; let restoreBtn = document.createElement("a"); restoreBtn.href = "#"; restoreBtn.textContent = "restore"; restoreBtn.style.cssText = "color:#36c; font-size:11px;"; restoreBtn.addEventListener("click", function(event) { event.preventDefault(); // Add it back as a pre-populated existing field row addCustomFieldRow(fieldName); // Remove the chip chip.remove(); // Hide the section if no more hidden fields if (hiddenSection.querySelectorAll("span[style]").length <= 1) hiddenSection.style.display = "none"; }); chip.appendChild(restoreBtn); hiddenSection.appendChild(chip); }); customFieldsSection.appendChild(hiddenSection); } manualInpDiv.appendChild(customFieldsSection); // Keep the position summary in sync when position fields change ["chrom", "start", "end"].forEach(function(id) { let el = advancedDiv.querySelector("#" + id); if (el) el.addEventListener("input", updatePositionSummary); }); let chromEl = advancedDiv.querySelector("#chrom"); let startEl = advancedDiv.querySelector("#start"); let endEl = advancedDiv.querySelector("#end"); if (chromEl && startEl && endEl) { let startVal = parseInt(startEl.value); let endVal = parseInt(endEl.value); let startFmt = isNaN(startVal) ? startEl.value : startVal.toLocaleString(); let endFmt = isNaN(endVal) ? endEl.value : endVal.toLocaleString(); posSummaryText.textContent = chromEl.value + ":" + startFmt + "-" + endFmt; } form.appendChild(manualInpDiv); // Add hidden field to encode form values as JSON const hiddenInput = document.createElement('input'); hiddenInput.type = 'hidden'; hiddenInput.name = 'hgt_doJsCommand'; form.appendChild(hiddenInput); const trackNameInput = document.createElement('input'); trackNameInput.type = 'hidden'; trackNameInput.id = 'trackName'; trackNameInput.name = 'trackName'; trackNameInput.value = "myVariants"; form.appendChild(trackNameInput); dialogEle.appendChild(form); // Initialize Spectrum color picker after form is added to DOM if (colorInput) { $(colorInput).spectrum({ hideAfterPaletteSelect: true, color: colorInput.value, showPalette: true, showInput: true, showSelectionPalette: true, showInitial: true, preferredFormat: "hex", localStorageKey: "myVariantsColors" }); } // Mount the block editor; reads Start/End from the chrom range fields. // Pass the container element directly: at this point the form is in // the dialog div but the dialog hasn't been appended to body yet, so // getElementById would still miss "blocksContainer". // Stash the widget handle on the form so createItem can call validate(). if (typeof myVariantsBlocks !== "undefined") { form.blocksWidget = myVariantsBlocks.mount(blocksContainer, { getStart: function () { return parseInt(document.getElementById("start").value, 10); }, getEnd: function () { return parseInt(document.getElementById("end").value, 10); }, hiddenCountInput: hCount, hiddenSizesInput: hSizes, hiddenStartsInput: hStarts }); } // Stash applyMode on the form so init() can restore last-used mode. form.applyMode = applyMode; return form; }, init: function () { // show a jquery-ui dialog when a user clicks on the 'Add Annotation' button let dialog = document.getElementById('myVariantsDialog'); if (!dialog) { dialog = document.createElement("div"); dialog.id = "myVariantsDialog"; dialog.style = "display: none"; dialogButtons = {}; // Call the function to build the form, but only if logged in already if (!userIsLoggedIn) { let msg = document.createElement("div"); msg.id = "logInMessage"; let href = (typeof myVariantsLoginUrl !== "undefined" && myVariantsLoginUrl) ? myVariantsLoginUrl : "./hgSession"; let link = document.createElement("a"); link.href = href; link.textContent = "log in"; msg.appendChild(document.createTextNode("Please ")); msg.appendChild(link); msg.appendChild(document.createTextNode(" to use this feature.")); dialog.appendChild(msg); } else { let form = this.createBedForm(dialog); document.body.append(dialog); dialogButtons.Submit = function() { // extract the form elements and check myVariants.createItem(form); }; } dialogButtons.Cancel = function(){ $(this).dialog("close"); }; $(dialog).dialog({ title: "My Annotations", resizable: false, height: "auto", width: 580, modal: true, closeOnEscape: true, autoOpen: false, buttons: dialogButtons, open: function() { // Restore the last input mode the user picked (or default to HGVS). let form = document.getElementById("myVariants-form"); if (form && form.applyMode) { let saved; try { saved = localStorage.getItem("myVariantsLastMode"); } catch (e) { saved = null; } const validModes = ["hgvs", "transcript", "snv", "cnv"]; form.applyMode(validModes.indexOf(saved) >= 0 ? saved : "hgvs"); } }, close: function() { // Reset block rows so the next open starts clean. let form = document.getElementById("myVariants-form"); if (form && form.blocksWidget) { form.blocksWidget.clear(); } } }); } else { // Reopening after an async image update; refresh the position fields. // The dialog's open callback re-applies the last-used mode. let form = document.getElementById("myVariants-form"); let start = form.elements.start; let end = form.elements.end; let thickStart = form.elements.thickStart; let thickEnd = form.elements.thickEnd; if (start) start.value = hgTracks.winStart; if (end) end.value = hgTracks.winEnd; if (thickStart) thickStart.value = hgTracks.winStart; if (thickEnd) thickEnd.value = hgTracks.winEnd; let summaryText = document.getElementById("positionSummaryText"); if (summaryText) { let chromEl = document.getElementById("chrom"); let startFmt = parseInt(hgTracks.winStart).toLocaleString(); let endFmt = parseInt(hgTracks.winEnd).toLocaleString(); summaryText.textContent = (chromEl ? chromEl.value : hgTracks.chromName) + ":" + startFmt + "-" + endFmt; } } // if we clicked outside of the pop up, close the popup: document.addEventListener('click', (e) => { let dialogEl = document.getElementById("myVariantsDialog"); if (!dialogEl) return; // If the click handler that ran first removed its own target from // the document (eg a row remove button, or a Grammarly/extension // DOM swap on the description field), e.target is now detached // and `.contains(e.target)` would falsely report "outside". Skip. if (!document.contains(e.target)) return; let dialogContainer = dialogEl.parentElement; // Check if click target is inside the dialog (handles native dropdowns that render outside bounds) if (dialogContainer && !dialogContainer.contains(e.target)) { $("#myVariantsDialog").dialog("close"); } }); }, createItem: function(form) { // sends a post to hgTracks that adds a new item to the users custom track // and updates the image to include this track if it wasn't already there // Block validation only matters for the transcript path; SNV/CNV don't carry blocks. let blockResult = {ok: true, noBlocks: true}; const isTranscript = !!(document.getElementById("typeTranscript") && document.getElementById("typeTranscript").checked); if (isTranscript && form.blocksWidget && form.blocksWidget.getRowCount() > 0) { blockResult = form.blocksWidget.validate(); if (!blockResult.ok) { alert("Block error: " + blockResult.msg); return; } } const data = {}; if (form.elements.hgvsInput.value) { data.hgvsInput = form.elements.hgvsInput.value; } else { const itemType = isTranscript ? "transcript" : (document.getElementById("typeCnv").checked ? "cnv" : "snv"); Array.from(form.elements).forEach( (ele) => { if (ele.name === "myVariantsHgvsInput" || ele.name === "hgt_doJsCommand" || ele.name === "myVariantsType" || (ele.tagName !== "INPUT" && ele.tagName !== "TEXTAREA")) {return;} const key = ele.id; let value = ele.value; // Handle Spectrum color picker - get the value from spectrum if available if (ele.id === "color" && $(ele).spectrum) { let spectrumColor = $(ele).spectrum("get"); if (spectrumColor) { value = spectrumColor.toHexString(); } } data[key] = value; }); data.itemType = itemType; data.cnvType = (itemType === "cnv") ? document.getElementById("cnvType").value : ""; if (itemType === "cnv") data.ref = ""; else if (itemType === "transcript") { data.ref = ""; data.alt = ""; } // Collect custom fields from the dynamic rows let customRows = document.querySelectorAll("#customFieldsList .customFieldRow"); if (customRows.length > 0) { let customFields = []; customRows.forEach(function(row) { let name = row.querySelector(".customFieldName").value.trim(); if (name) { let value = row.querySelector(".customFieldValue").value; customFields.push({name: name, value: value}); } }); if (customFields.length > 0) { data.extraFields = customFields; } } // Convert hidden block fields from CSV strings to arrays of ints. // Drop blocks entirely for SNV/CNV; server synthesizes a single full-span // block. For transcript with noBlocks (rows added but every size empty), // also drop and let the server synthesize. let bc = parseInt(data.blockCount, 10); if (!isTranscript || !bc || blockResult.noBlocks) { delete data.blockCount; delete data.blockSizes; delete data.chromStarts; } else { data.blockCount = bc; data.blockSizes = data.blockSizes.split(",").map(Number); data.chromStarts = data.chromStarts.split(",").map(Number); } } // Show loading indicator const loadingId = showLoadingImage("imgTbl"); document.body.style.cursor = "wait"; // Build request - use fetch() instead of form.submit() const trackName = form.elements.namedItem("trackName").value; const req = encodeURIComponent(`myVariants ${trackName} ${JSON.stringify(data)}`); const url = cart.addUpdatesToUrl(`../cgi-bin/hgTracks?hgt_doJsCommand=${req}&trackName=${trackName}&hgt.trackImgOnly=1&hgt.ideogramToo=1&hgsid=${getHgsid()}&db=${getDb()}`); fetch(url, { method: "POST", credentials: "same-origin" }) .then(response => { if (!response.ok) { throw new Error("Network response was not ok: " + response.status); } return response.text(); }) .then(html => { hideLoadingImage(loadingId); document.body.style.cursor = ""; myVariants.handleCreateSuccess(html, data); }) .catch(error => { hideLoadingImage(loadingId); document.body.style.cursor = ""; warn("Error creating annotation: " + error.message); console.error("Fetch error:", error); }); }, handleCreateSuccess: function(response, data) { // Close the create dialog $("#myVariantsDialog").dialog("close"); // Update the image using the existing pattern imageV2.updateImgAndMap.call({cmd: 'wholeImage'}, response, 'success'); // Extract new item coordinates from server response const newItemPos = scrapeVariable(response, "newItemPos"); let variantChrom, variantStart, variantEnd; if (newItemPos) { // Server returned the coordinates variantChrom = newItemPos.chrom; variantStart = newItemPos.start; variantEnd = newItemPos.end; } else { // Fallback: try to get from form data (manual entry) variantChrom = data.chrom || hgTracks.chromName; variantStart = parseInt(data.start, 10) || hgTracks.winStart; variantEnd = parseInt(data.end, 10) || hgTracks.winEnd; } // Check if variant is in current window const inCurrentWindow = (variantChrom === hgTracks.chromName && variantStart < hgTracks.winEnd && variantEnd > hgTracks.winStart); if (inCurrentWindow) { // Already visible - just show brief success message return; } // Check stored preference const navPref = localStorage.getItem("myVariants_navPref"); if (navPref === "jump") { // Auto-navigate to variant myVariants.navigateToVariant(variantChrom, variantStart, variantEnd); return; } else if (navPref === "stay") { // Stay here, just show message warn("Annotation created at " + variantChrom + ":" + (variantStart+1).toLocaleString() + "-" + variantEnd.toLocaleString()); return; } // No preference saved - show dialog myVariants.showNavigationDialog(variantChrom, variantStart, variantEnd); }, showNavigationDialog: function(chrom, start, end) { const posStr = chrom + ":" + (start+1).toLocaleString() + "-" + end.toLocaleString(); // Create dialog content const content = document.createElement("div"); // Build message with position const msg = document.createElement("p"); msg.style.marginBottom = "0.5em"; msg.innerHTML = "Annotation created at " + posStr + ""; content.appendChild(msg); // Show dialog with buttons $(content).dialog({ title: "Annotation Created", modal: true, width: 600, buttons: { "Go to Annotation": function() { if (document.getElementById("myVariantsRememberNav").checked) { localStorage.setItem("myVariants_navPref", "jump"); } $(this).dialog("close"); myVariants.navigateToVariant(chrom, start, end); }, "Stay Here": function() { if (document.getElementById("myVariantsRememberNav").checked) { localStorage.setItem("myVariants_navPref", "stay"); } $(this).dialog("close"); } }, open: function() { // Add checkbox to button pane, inline with buttons const dialog = $(this); const buttonPane = dialog.closest(".ui-dialog").find(".ui-dialog-buttonpane"); const checkboxSpan = document.createElement("span"); checkboxSpan.style.cssText = "display: inline-block; vertical-align: middle; margin-right: 1em;"; checkboxSpan.innerHTML = ""; checkboxSpan.appendChild(createInfoIcon( "Save your preference. Future annotations outside the current view will automatically " + "use this choice. Reset via Configure page or cartReset." )); // Insert before the button set buttonPane.find(".ui-dialog-buttonset").before(checkboxSpan); // Force dialog to recalculate its size dialog.dialog("option", "height", "auto"); } }); }, navigateToVariant: function(chrom, start, end) { // Add small padding around the variant (max 100bp) const padding = 100; const paddedStart = Math.max(0, start - padding); const paddedEnd = end + padding; // Navigate using existing mechanism const pos = chrom + ":" + paddedStart + "-" + paddedEnd; imageV2.navigateInPlace("position=" + pos, null, true); }, showDialog: function() { let dialog = document.getElementById('myVariantsDialog'); // Clear dynamically-added custom field rows; for existing fields, just clear the value let customFieldsList = document.getElementById("customFieldsList"); if (customFieldsList) { let rows = customFieldsList.querySelectorAll(".customFieldRow"); rows.forEach(function(row) { if (row.dataset.existing === "true") { // Existing field - just clear the value input let valInput = row.querySelector(".customFieldValue"); if (valInput) valInput.value = ""; } else { // Dynamically added row - remove it row.remove(); } }); } $(dialog).dialog("open"); }, }; ///////////////// //// posting //// ///////////////// var 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 we clicked on a merged item then show all the items, similar to clicking a // dense track to turn it to pack if (!(this && this.href)) { writeToApacheLog(`no href for mapClk, this = `); } let parsedUrl = parseUrl(this.href); let cgi = parsedUrl.cgi; // for some reason, '-' characters are encoded here? unencode them so lookups into // hgTracks.trackDb will work let id = ""; if (typeof parsedUrl.queryArgs.g !== "undefined") { id = parsedUrl.queryArgs.g.replace("%2D", "-"); } if (parsedUrl.queryArgs.i === "mergedItem") { updateObj={}; updateObj[id+".doMergeItems"] = 0; hgTracks.trackDb[id][id+".doMergeItems"] = 0; cart.setVarsObj(updateObj,null,false); imageV2.requestImgUpdate(id, id + ".doMergeItems=0"); return false; } if (done) return false; else { // first check if we are allowed to click, we could be in the middle // of a drag select if (posting.blockUseMap === true) { return false; } else if (cgi === "hgGene") { window.hgcOrHgGeneArgs = parsedUrl.queryArgs; id = parsedUrl.queryArgs.hgg_type; popUpHgcOrHgGene.hgc(id, this.href); return false; } else if (cgi === "hgTrackUi") { rec = hgTracks.trackDb[id]; if (rec && tdbIsLeaf(rec)) { popUp.hgTrackUi(id, false); } else { location.assign(href); } } else if (cgi === "hgc") { if (id.startsWith("multiz")) { // multiz tracks have a form that lets you change highlighted bases // that does not play well in a pop up // toga tracks require bootstrap which does not work with something location.assign(href); return false; } popUpHgcOrHgGene.hgc(id, this.href); return false; } else { // must be changing the density of a track, save the settings and post the form: return posting.saveSettings(this); } } }, saveSettings: function (obj) { if (posting.blockUseMap === true) { return false; } if (!obj || !obj.href) // called directly with obj obj = this; // and from callback without obj if ($(obj).hasClass('noLink')) // TITLE_BUT_NO_LINK return false; if (obj.href.match('#') || obj.target.length > 0) { //alert("Matched # ["+obj.href+"] or has target:"+obj.target); return true; } var thisForm = normed($(obj).parents('form').first()); if (!thisForm) thisForm = normed($("FORM").first()); if (thisForm) { //alert("posting form:"+$(thisForm).attr('name')); return postTheForm($(thisForm).attr('name'),cart.addUpdatesToUrl(obj.href)); } return true; } }; ///////////////////////// //// cart updating ///// /////////////////////// var cart = { // Controls queuing and ultimately updating cart variables vis ajax or submit. Queued vars // are held in an object with unique keys preventing duplicate updates and ensuring last update // takes precedence. WARNING: be careful creating an object with variables on the fly: // cart.setVarsObj({track: vis}) is invalid but cart.setVarsObj({'knownGene': vis}) is ok! updateQueue: {}, updatesWaiting: function () { // returns TRUE if updates are waiting. return objNotEmpty(cart.updateQueue); }, addUpdatesToUrl: function (url) { // adds any outstanding cart updates to the url, then clears the queue if (cart.updatesWaiting()) { //console.log('cart.addUpdatesToUrl: '+objKeyCount(cart.updateQueue)+' vars'); var updates = cart.varsToUrlData(); // clears the queue if (!url || url.length === 0) return updates; if (updates.length > 0) { var dataOnly = (url.indexOf("cgi-bin") === -1); // all urls should be to our cgis if (!dataOnly && url.lastIndexOf("?") === -1) url += "?" + updates; else url += '&' + updates; } } return url; }, varsToUrlData: function (varsObj) { // creates a url data (var1=val1&var2=val2...) string from vars object and queue // The queue will be emptied by this call. cart.queueVarsObj(varsObj); // lay ontop of queue, to give new values precedence // Now convert to url data and clear queue var urlData = ''; if (cart.updatesWaiting()) { //console.log('cart.varsToUrlData: '+objKeyCount(cart.updateQueue)+' vars'); urlData = varHashToQueryString(cart.updateQueue); cart.updateQueue = {}; } return urlData; }, setVarsObj: function (varsObj, errFunc, async) { // Set all vars in a var hash, appending any queued updates // The default behavior is async = true //console.log('cart.setVarsObj: were:'+objKeyCount(cart.updateQueue) + // ' new:'+objKeyCount(varsObj); cart.queueVarsObj(varsObj); // lay ontop of queue, to give new values precedence // Now ajax update all in queue and clear queue if (cart.updatesWaiting()) { setVarsFromHash(cart.updateQueue, errFunc, async); cart.updateQueue = {}; } }, setVars: function (names, values, errFunc, async) { // ajax updates the cart, and includes any queued updates. cart.updateSessionPanel(); // handles hide from left minibutton cart.setVarsObj(arysToObj(names, values), errFunc, async); }, queueVarsObj: function (varsObj) { // Add object worth of cart updates to the 'to be updated' queue, so they can be sent to // the server later. Note: hash allows overwriting previous updates to the same variable. if (typeof varsObj !== 'undefined' && objNotEmpty(varsObj)) { //console.log('cart.queueVarsObj: were:'+objKeyCount(cart.updateQueue) + // ' new:'+objKeyCount(varsObj)); for (var name in varsObj) { cart.updateQueue[name] = varsObj[name]; } } }, addVarsToQueue: function (names,values) { // creates a string of updates to save for ajax batch or a submit cart.queueVarsObj(arysToObj(names,values)); }, updateSessionPanel: function() /* This function tries to find out if the current cart changed enough. It is not used anymore, as the * current active rec. track set name is not shown anymore */ { // this is never set anymore if (typeof recTrackSetsDetectChanges === 'undefined' || recTrackSetsDetectChanges === null) return; // change color of text $('span.gbSessionChangeIndicator').addClass('gbSessionChanged'); // change mouseover on the panel. A bit fragile here inserting text in the mouseover specified in // hgTracks.js, so depends on match with text there, and should present same message as C code // (Perhaps this could be added as a script tag, so not duplicated) var txt = $('span.gbSessionLabelPanel').attr('title'); if (txt && !txt.match(/with changes/)) { $('span.gbSessionLabelPanel').attr('title', txt.replace( "track set", "track set, with changes (added or removed tracks) you have requested")); } } }; /////////////////////////////////////////////// //// visibility (mixed with group toggle) ///// /////////////////////////////////////////////// var vis = { // map cgi enum visibility codes to strings enumOrder: new Array("hide", "dense", "full", "pack", "squish"), update: function (track, visibility) { // Updates visibility state in hgTracks.trackDb and any visible elements on the page. // returns true if we modify at least one select in the group list var rec = hgTracks.trackDb[track]; var selectUpdated = false; $("select[name=" + escapeJQuerySelectorChars(track) + "]").each(function(t) { $(this).attr('class', visibility === 'hide' ? 'hiddenText' : 'normalText'); $(this).val(visibility); selectUpdated = true; }); if (rec) { rec.localVisibility = visibility; } return selectUpdated; }, get: function (track) { // return current visibility for given track var rec = hgTracks.trackDb[track]; if (rec) { if (rec.localVisibility) { return rec.localVisibility; } else { return vis.enumOrder[rec.visibility]; } } else { return null; } }, makeTrackVisible: function (track) { // Sets the vis box to visible, and ques a cart update, but does not update the image // Trusts that the cart update will be submitted later. if (track && vis.get(track) !== "full") { vis.update(track, 'pack'); cart.addVarsToQueue([track], ['pack']); } }, toggleForGroup: function (button, prefix) { // toggle visibility of a track group; prefix is the prefix of all the id's of tr's in the // relevant group. This code also modifies the corresponding hidden fields and the gif // of the +/- img tag. imageV2.markAsDirtyPage(); if (arguments.length > 2) return setTableRowVisibility(button, prefix, "hgtgroup", "group",false,arguments[2]); else return setTableRowVisibility(button, prefix, "hgtgroup", "group", true); }, expandAllGroups: function (newState) { // Set visibility of all track groups to newState (true means expanded). // This code also modifies the corresponding hidden fields and the gif's of the +/- img tag. imageV2.markAsDirtyPage(); $(".toggleButton[id$='_button']").each( function (i) { // works for old img type AND new BUTTONS_BY_CSS // - 7: clip '_button' suffix vis.toggleForGroup(this,this.id.substring(0,this.id.length - 7),newState); }); return false; }, initForAjax: function() { // To better support the back-button, it is good to eliminate extraneous form puts // Towards that end, we support visBoxes making ajax calls to update cart. var sels = $('select.normalText,select.hiddenText'); $(sels).on("change", function() { var track = $(this).attr('name'); let newVis = $(this).val(); if (newVis === 'hide') { var rec = hgTracks.trackDb[track]; if (rec) rec.visibility = 0; // else Would be nice to hide subtracks as well but that may be overkill $(document.getElementById('tr_' + track)).remove(); cart.updateSessionPanel(); imageV2.drawHighlights(); $(this).attr('class', 'hiddenText'); } else $(this).attr('class', 'normalText'); document.querySelectorAll('[name="'+track+'"]').forEach( (sel) => { sel.value = newVis; }); cart.setVars([track], [$(this).val()]); imageV2.markAsDirtyPage(); return false; }); // Now we can rid the submt of the burden of all those vis boxes var form = $('form#TrackForm'); $(form).on("submit", function () { $('select.normalText,select.hiddenText').prop('disabled',true); }); $(form).attr('method','get'); }, restoreFromBackButton: function() // Re-enabling vis dropdowns is necessary because initForAjax() disables them on submit. { $('select.normalText,select.hiddenText').prop('disabled',false); } }; //////////////////////////////////////////////////////////// // dragSelect is also known as dragZoom or shift-dragZoom // //////////////////////////////////////////////////////////// var dragSelect = { hlColor : '#aac6ff', // current highlight color hlColorDefault: '#aac6ff', // default highlight color, if nothing specified areaSelector: null, // formerly "imgAreaSelect". jQuery element used for imgAreaSelect originalCursor: null, startTime: null, escPressed : false, // flag is set when user presses Escape selectStart: function (img, selection) { initVars(); dragSelect.escPressed = false; 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; }, findHighlightIdxForPos : function(findPos) { // return the index of the highlight string e.g. hg19.chrom1:123-345#AABBDCC that includes a chrom range findPos // mostly copied from drawHighlights() var currDb = getDb(); if (hgTracks.highlight) { var hlArray = hgTracks.highlight.split("|"); // support multiple highlight items for (var i = 0; i < hlArray.length; i++) { hlString = hlArray[i]; pos = parsePositionWithDb(hlString); imageV2.undisguiseHighlight(pos); if (!pos) continue; // ignore invalid position strings pos.start--; if (pos.chrom === hgTracks.chromName && pos.db === currDb && pos.start <= findPos.chromStart && pos.end >= findPos.chromEnd) { return i; } } } return null; }, saveHlColor : function (hlColor) // save the current hlColor to the object and also the cart variable hlColor and return it. // hlColor is a 6-character hex string prefixed by # { dragSelect.hlColor = hlColor; cart.setVars(["prevHlColor"], [dragSelect.hlColor], null, false); hgTracks.prevHlColor = hlColor; // cart.setVars does not update the hgTracks-variables. The cart-variable system is a problem. return hlColor; }, loadHlColor : function () // load hlColor from prevHlColor in the cart, or use default color, set and return it // color is a 6-char hex string prefixed by # { if (hgTracks.prevHlColor) dragSelect.hlColor = hgTracks.prevHlColor; else dragSelect.hlColor = dragSelect.hlColorDefault; return dragSelect.hlColor; }, highlightThisRegion: function(newPosition, doAdd, hlColor) // set highlighting newPosition in server-side cart and apply the highlighting in local UI. // hlColor can be undefined, in which case it defaults to the last used color or the default light blue // if hlColor is set, it is also saved into the cart. // if doAdd is true, the highlight is added to the current list. If it is false, all old highlights are deleted. { newPosition.replace("virt:", "multi:"); var hlColorName = null; if (hlColor==="" || hlColor===null || hlColor===undefined) hlColorName = dragSelect.loadHlColor(); else hlColorName = dragSelect.saveHlColor( hlColor ); var pos = parsePosition(newPosition); var start = pos.start; var end = pos.end; var newHighlight = makeHighlightString(getDb(), pos.chrom, start, end, hlColorName); newHighlight = imageV2.disguiseHighlight(newHighlight); var oldHighlight = hgTracks.highlight; if (oldHighlight===undefined || doAdd===undefined || doAdd===false || oldHighlight==="") { // just set/overwrite the old highlight position, this used to be the default hgTracks.highlight = newHighlight; } else { // add to the end of a |-separated list hgTracks.highlight = oldHighlight+"|"+newHighlight; } // we include enableHighlightingDialog because it may have been changed by the dialog var cartSettings = { 'highlight': hgTracks.highlight, 'enableHighlightingDialog': hgTracks.enableHighlightingDialog ? 1 : 0 }; if (hgTracks.windows && !hgTracks.virtualSingleChrom) { var nonVirtChrom = ""; var nonVirtStart = -1; var nonVirtEnd = -1; for (i=0,len=hgTracks.windows.length; i < len; ++i) { var w = hgTracks.windows[i]; // overlap with new position? if (w.virtEnd > start && end > w.virtStart) { var s = Math.max(start, w.virtStart); var e = Math.min(end, w.virtEnd); var cs = s - w.virtStart + w.winStart; var ce = e - w.virtStart + w.winStart; if (nonVirtChrom === "") { nonVirtChrom = w.chromName; nonVirtStart = cs; nonVirtEnd = ce; } else { if (w.chromName === nonVirtChrom) { nonVirtEnd = Math.max(ce, nonVirtEnd); } else { break; } } } } if (nonVirtChrom !== "") cartSettings.nonVirtHighlight = makeHighlightString(getDb(), nonVirtChrom, nonVirtStart, (nonVirtEnd+1), hlColorName); } else if (hgTracks.windows && hgTracks.virtualSingleChrom) { cartSettings.nonVirtHighlight = hgTracks.highlight; } // TODO if not virt, do we need to erase cart nonVirtHighlight ? cart.setVarsObj(cartSettings); imageV2.drawHighlights(); }, selectionEndDialog: function (newPosition) // Let user choose between zoom-in and highlighting. { newPosition.replace("virt:", "multi:"); // if the user hit Escape just before, do not show this dialo if (dragSelect.startTime===null) return; var dragSelectDialog = $("#dragSelectDialog")[0]; if (!dragSelectDialog) { $("body").append("
" + "

"); makeHighlightPicker("hlColor", document.getElementById("dragSelectDialog"), null); $("#dragSelectDialog").append("
" + "Don't show this again and always zoom with shift.
" + "Re-enable via 'View - Configure Browser' (c then f)
"+ "Selected chromosome position: "); dragSelectDialog = $("#dragSelectDialog")[0]; // reset value // allow to click checkbox by clicking on the label $('#hlNotShowAgainMsg').on("click", function() { $('#disableDragHighlight').trigger("click");}); // click "add highlight" when enter is pressed in color input box $("#hlColorInput").on("keyup", function(event){ if(event.keyCode == 13){ $(".ui-dialog-buttonset button:nth-child(3)").trigger("click"); } }); } if (hgTracks.windows) { var i,len; var newerPosition = newPosition; if (hgTracks.virtualSingleChrom && (newPosition.search("multi:")===0)) { newerPosition = genomePos.disguisePosition(newPosition); } var str = newerPosition + "
\n"; var str2 = "
\n"; str2 += "\n"; if (!(hgTracks.virtualSingleChrom && (selectedRegions === 1))) { str += str2; } $("#dragSelectPosition").html(str); } else { $("#dragSelectPosition").html(newPosition); } // Build the button set in two passes so that "Create Item" only // appears when myVariants is enabled in hg.conf. doMyVariants is // emitted by the server only inside the gated hg.conf block, so on // a server with the feature off the variable is undefined and the // button is omitted entirely. let dragSelectButtons = { "Zoom In": function() { // Zoom to selection $(this).dialog("option", "revertToOriginalPos", false); if ($("#disableDragHighlight").prop('checked')) hgTracks.enableHighlightingDialog = false; if (imageV2.inPlaceUpdate) { if (hgTracks.virtualSingleChrom && (newPosition.search("multi:")===0)) { newPosition = genomePos.disguisePosition(newPosition); // DISGUISE } var params = "db=" + getDb() + "&position=" + newPosition; if (!hgTracks.enableHighlightingDialog) params += "&enableHighlightingDialog=0"; imageV2.navigateInPlace(params, null, true); } else { $('body').css('cursor', 'wait'); if (!hgTracks.enableHighlightingDialog) cart.setVarsObj({'enableHighlightingDialog': 0 },null,false); // async=false document.TrackHeaderForm.submit(); } $(this).dialog("close"); }, "Single Highlight": function() { // Clear old highlight and Highlight selection $(imageV2.imgTbl).imgAreaSelect({hide:true}); if ($("#disableDragHighlight").prop('checked')) hgTracks.enableHighlightingDialog = false; var hlColor = $("#hlColorInput").val(); dragSelect.highlightThisRegion(newPosition, false, hlColor); $(this).dialog("close"); }, "Add Highlight": function() { // Highlight selection if ($("#disableDragHighlight").prop('checked')) hgTracks.enableHighlightingDialog = false; var hlColor = $("#hlColorInput").val(); dragSelect.highlightThisRegion(newPosition, true, hlColor); $(this).dialog("close"); }, "Save Color": function() { var hlColor = $("#hlColorInput").val(); dragSelect.saveHlColor( hlColor ); $(this).dialog("close"); } }; if (typeof doMyVariants !== 'undefined' && doMyVariants) { dragSelectButtons["Add Annotation"] = function() { let data = {}; let pos = parsePosition(newPosition); data.chrom = pos.chrom; data.start = data.thickStart = pos.start.toString(); data.end = data.thickEnd = pos.end.toString(); data.score = "0"; data.strand = "."; data.color = $("#hlColorInput").val(); data.name = ""; data.description = ""; data.ref = ""; data.alt = ""; data.trackName = "myVariants"; $(this).dialog("close"); let req = encodeURIComponent(`myVariants myVariants ${JSON.stringify(data)}`); jQuery('body').css('cursor', 'wait'); $.ajax({ type: "POST", url: "../cgi-bin/hgTracks", data: cart.addUpdatesToUrl(`hgt_doJsCommand=${req}&trackName=myVariants&hgsid=${getHgsid()}`), dataType: "html", trueSuccess: imageV2.updateImgAndMap, success: catchErrorOrDispatch, error: errorHandler, cmd: 'wholeImage', loadingId: showLoadingImage("imgTbl"), cache: false }); }; } dragSelectButtons.Cancel = function() { $(this).dialog("close"); }; $(dragSelectDialog).dialog({ modal: true, title: "Drag-and-select", closeOnEscape: true, resizable: false, autoOpen: false, revertToOriginalPos: true, minWidth: 750, buttons: dragSelectButtons, open: function () { // Make zoom the focus/default action $(this).parents('.ui-dialog-buttonpane button:eq(0)').trigger("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").prop('checked')) $(this).remove(); else $(this).hide(); $('body').css('cursor', ''); // Occasionally wait cursor got left behind $("#hlColorPicker").spectrum("hide"); } }); $(dragSelectDialog).dialog('open'); }, selectEnd: function (img, selection, event) { var now = new Date(); var doIt = false; var rulerClicked = selection.y1 <= hgTracks.rulerClickHeight; // = drag on base position track (no shift) if (dragSelect.originalCursor) jQuery('body').css('cursor', dragSelect.originalCursor); if (dragSelect.escPressed) return false; // ignore releases outside of the image rectangle (allowing a 10 pixel slop) 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 || rulerClicked); } 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); newPosition.replace("virt:", "multi:"); if (newPosition) { if (event.altKey) { // with the alt-key, only highlight the region, do not zoom dragSelect.highlightThisRegion(newPosition, true); $(imageV2.imgTbl).imgAreaSelect({hide:true}); } else { if (hgTracks.enableHighlightingDialog && !(event.metaKey || event.ctrlKey)) // don't show the dialog if: clicked on ruler, if dialog deactivated or meta/ctrl was pressed dragSelect.selectionEndDialog(newPosition); else { // in every other case, show the dialog $(imageV2.imgTbl).imgAreaSelect({hide:true}); if (imageV2.inPlaceUpdate) { if (hgTracks.virtualSingleChrom && (newPosition.search("multi:")===0)) { newPosition = genomePos.disguisePosition(newPosition); // DISGUISE } imageV2.navigateInPlace("db=" + getDb() + "&position=" + newPosition, null, true); } else { jQuery('body').css('cursor', 'wait'); document.TrackHeaderForm.submit(); } } } } else { $(imageV2.imgTbl).imgAreaSelect({hide:true}); genomePos.revertToOriginalPos(); } dragSelect.startTime = null; // 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.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; var xLimits = genomePos.getXLimits($(imageV2.imgTbl), 0); dragSelect.areaSelector = jQuery((imageV2.imgTbl).imgAreaSelect({ minX : xLimits[0], maxX : xLimits[1], selectionColor: 'blue', outerColor: '', minHeight: imgHeight, maxHeight: imgHeight, onSelectStart: dragSelect.selectStart, onSelectChange: dragSelect.selectChange, onSelectEnd: dragSelect.selectEnd, autoHide: false, // gets hidden after possible dialog movable: false, clickClipHeight: heights })); // remove any ongoing drag-selects when the esc key is pressed anywhere for this document // This allows to abort zooming / highlighting $(document).on("keyup", function(e){ if(e.keyCode === 27) { $(imageV2.imgTbl).imgAreaSelect({hide:true}); dragSelect.escPressed = true; } }); // hide and redraw all current highlights when the browser window is resized $(window).on("resize", function() { $(imageV2.imgTbl).imgAreaSelect({hide:true}); imageV2.drawHighlights(); }); } } }; ///////////////////////////////////// //// 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 // mouseUp: if no drag, then create href centered on bpDown loc with current span // if drag, then create href from bpDown to bpUp // if ctrlKey then expand selection to containing cytoBand(s) // Image dimensions all in pix var img = { top: -1, scrolledTop: -1, height: -1, left: -1, scrolledLeft: -1, width: -1 }; // chrom Dimensions beg,end,size in bases, rest in pix var chr = { name: "", reverse: false, beg: -1, end: -1, size: -1, top: -1, bottom: -1, left: -1, right: -1, width: -1 }; var pxDown = 0; // pix X location of mouseDown var chrImg = $(this); var mouseIsDown = false; var mouseHasMoved = false; var hilite = null; initialize(); function initialize(){ findDimensions(); if (chr.top === -1) warn("chromIdeo(): failed to register "+this.id); else { hiliteSetup(); $('area.cytoBand').off('mousedown'); // Make sure this is only bound once $('area.cytoBand').on("mousedown", function(e) { // mousedown on chrom portion of image only (map items) updateImgOffsets(); pxDown = e.clientX - img.scrolledLeft; var pxY = e.clientY - img.scrolledTop; if (mouseIsDown === false && isWithin(chr.left,pxDown,chr.right) && isWithin(chr.top,pxY,chr.bottom)) { mouseIsDown = true; mouseHasMoved = false; $(document).on('mousemove',chromMove); $(document).on( 'mouseup', chromUp); hiliteShow(pxDown,pxDown); return false; } }); } } function chromMove(e) { // If mouse was down, determine if dragged, then show hilite if ( mouseIsDown ) { var pxX = e.clientX - img.scrolledLeft; var relativeX = (pxX - pxDown); if (mouseHasMoved || (mouseHasMoved === false && Math.abs(relativeX) > 2)) { mouseHasMoved = true; if (isWithin(chr.left,pxX,chr.right)) hiliteShow(pxDown,pxX); else if (pxX < chr.left) hiliteShow(pxDown,chr.left); else hiliteShow(pxDown,chr.right); } } } function chromUp(e) { // If mouse was down, handle final selection $(document).off('mousemove',chromMove); $(document).off('mouseup',chromUp); chromMove(e); // Just in case if (mouseIsDown) { updateImgOffsets(); var bands; var pxUp = e.clientX - img.scrolledLeft; var pxY = e.clientY - img.scrolledTop; if (isWithin(0,pxY,img.height)) { // within vertical range or else cancel var selRange = { beg: -1, end: -1, width: -1 }; var dontAsk = true; if (e.ctrlKey) { bands = findCytoBand(pxDown,pxUp); if (bands.end > -1) { pxDown = bands.left; pxUp = bands.right; mouseHasMoved = true; dontAsk = false; selRange.beg = bands.beg; selRange.end = bands.end; hiliteShow(pxDown,pxUp); } } else if (mouseHasMoved) { // bounded by chrom dimensions: but must remain within image! if (isWithin(-20,pxUp,chr.left)) pxUp = chr.left; if (isWithin(chr.right,pxUp,img.width + 20)) pxUp = chr.right; if ( isWithin(chr.left,pxUp,chr.right+1) ) { selRange.beg = convertToBases(pxDown); selRange.end = convertToBases(pxUp); if (Math.abs(selRange.end - selRange.beg) < 20) mouseHasMoved = false; // Drag so small: treat as simple click else dontAsk = false; } } if (mouseHasMoved === false) { // Not else because small drag turns this off hiliteShow(pxUp,pxUp); var curWidth = hgTracks.winEnd - hgTracks.winStart; // Notice that beg is based upon up position selRange.beg = convertToBases(pxUp) - Math.round(curWidth/2); selRange.end = selRange.beg + curWidth; } if (selRange.end > -1) { // prompt, then submit for new position selRange = rangeNormalizeToChrom(selRange,chr); if (mouseHasMoved === false) { // Update highlight by converting bp back to pix pxDown = convertFromBases(selRange.beg); pxUp = convertFromBases(selRange.end); hiliteShow(pxDown,pxUp); } //if ((selRange.end - selRange.beg) < 50000) // dontAsk = true; if (dontAsk || confirm("Jump to new position:\n\n"+chr.name+":"+commify(selRange.beg)+ "-"+commify(selRange.end)+" size:"+commify(selRange.width)) ) { genomePos.setByCoordinates(chr.name, selRange.beg, selRange.end); // Stop the presses :0) $('area.cytoBand').on("mousedown", function(e) { return false; }); if (imageV2.backSupport) { imageV2.navigateInPlace("db=" + getDb() + "&position=" + encodeURIComponent(genomePos.get().replace(/,/g,'')) + "&findNearest=1",null,true); hiliteCancel(); } else document.TrackHeaderForm.submit(); return true; // Make sure the setTimeout below is not called. } } } hiliteCancel(); setTimeout(posting.allowMapClicks,50); } mouseIsDown = false; mouseHasMoved = false; } function isWithin(beg,here,end) { // Simple utility return ( beg <= here && here < end ); } function convertToBases(pxX) { // Simple utility to convert pix to bases var offset = (pxX - chr.left)/chr.width; if (chr.reverse) offset = 1 - offset; return Math.round(offset * chr.size); } function convertFromBases(bases) { // Simple utility to convert bases to pix var offset = bases/chr.size; if (chr.reverse) offset = 1 - offset; return Math.round(offset * chr.width) + chr.left; } function findDimensions() { // Called at init: determine the dimensions of chrom from 'cytoband' map items var lastX = -1; $('area.cytoBand').each(function(ix) { var loc = this.coords.split(","); if (loc.length === 4) { var myLeft = parseInt(loc[0]); var myRight = parseInt(loc[2]); if (chr.top === -1) { chr.left = myLeft; chr.right = myRight; chr.top = parseInt(loc[1]); chr.bottom = parseInt(loc[3]); } else { if (chr.left > myLeft) chr.left = myLeft; if (chr.right < parseInt(loc[2])) chr.right = parseInt(loc[2]); } let title = this.getAttribute("data-tooltip") || this.getAttribute("title"); var range = title.substr(title.lastIndexOf(':')+1); var pos = range.split('-'); if (pos.length === 2) { if (chr.name.length === 0) { chr.beg = parseInt(pos[0]); //chr.end = parseInt(pos[1]); chr.name = title.substring(title.lastIndexOf(' ')+1, title.lastIndexOf(':')); } else { if (chr.beg > parseInt(pos[0])) chr.beg = parseInt(pos[0]); } if (chr.end < parseInt(pos[1])) { chr.end = parseInt(pos[1]); if (lastX === -1) lastX = myRight; else if (lastX > myRight) chr.reverse = true; // end is advancing, but X is not, so reverse } else if (lastX !== -1 && lastX < myRight) chr.reverse = true; // end is not advancing, but X is, so reverse } $(this).css( 'cursor', 'text'); $(this).attr("href",""); } }); chr.size = (chr.end - chr.beg ); chr.width = (chr.right - chr.left); } function findCytoBand(pxDown,pxUp) { // Called when mouseup and ctrl: Find the bounding cytoband dimensions (in pix and bases) var cyto = { left: -1, right: -1, beg: -1, end: -1 }; $('area.cytoBand').each(function(ix) { var loc = this.coords.split(","); if (loc.length === 4) { var myLeft = parseInt(loc[0]); var myRight = parseInt(loc[2]); var range; var pos; if (cyto.left === -1 || cyto.left > myLeft) { if ( isWithin(myLeft,pxDown,myRight) || isWithin(myLeft,pxUp,myRight) ) { cyto.left = myLeft; range = this.title.substr(this.title.lastIndexOf(':')+1); pos = range.split('-'); if (pos.length === 2) { cyto.beg = (chr.reverse ? parseInt(pos[1]) : parseInt(pos[0])); } } } if (cyto.right === -1 || cyto.right < myRight) { if ( isWithin(myLeft,pxDown,myRight) || isWithin(myLeft,pxUp,myRight) ) { cyto.right = myRight; range = this.title.substr(this.title.lastIndexOf(':')+1); pos = range.split('-'); if (pos.length === 2) { cyto.end = (chr.reverse ? parseInt(pos[0]) : parseInt(pos[1])); } } } } }); return cyto; } function rangeNormalizeToChrom(selection,chrom) { // Called before presenting or using base range: make sure chrom selection // is within chrom range if (selection.end < selection.beg) { var tmp = selection.end; selection.end = selection.beg; selection.beg = tmp; } selection.width = (selection.end - selection.beg); selection.beg += 1; if (selection.beg < chrom.beg) { selection.beg = chrom.beg; selection.end = chrom.beg + selection.width; } if (selection.end > chrom.end) { selection.end = chrom.end; selection.beg = chrom.end - selection.width; if (selection.beg < chrom.beg) { // spans whole chrom selection.width = (selection.end - chrom.beg); selection.beg = chrom.beg + 1; } } return selection; } function hiliteShow(down,cur) { // Called on mousemove, mouseup: set drag hilite dimensions var topY = img.top; var high = img.height; var begX = -1; var wide = -1; if (cur < down) { begX = cur + img.left; wide = (down - cur); } else { begX = down + img.left; wide = (cur - down); } $(hilite).css({ left: begX + 'px', width: wide + 'px', top: topY + 'px', height: high + 'px', display:'' }); $(hilite).show(); } function hiliteCancel(left,width,top,height) { // Called on mouseup: Make green drag hilite disappear when no longer wanted $(hilite).hide(); $(hilite).css({ left: '0px', width: '0px', top: '0px', height: '0px' }); } function hiliteSetup() { // Called on init: setup of drag region hilite (but don't show yet) if (hilite === null) { // setup only once hilite = jQuery("
"); $(hilite).css({ backgroundColor: 'green', opacity: 0.4, borderStyle: 'solid', borderWidth: '1px', bordercolor: '#0000FF' }); $(hilite).css({ display: 'none', position: 'absolute', overflow: 'hidden', zIndex: 1 }); jQuery($(chrImg).parents('body')).append($(hilite)); } return hilite; } function updateImgOffsets() { // Called on mousedown: Gets the current offsets var offs = $(chrImg).offset(); img.top = Math.round(offs.top ); img.left = Math.round(offs.left); img.scrolledTop = img.top - $("body").scrollTop(); img.scrolledLeft = img.left - $("body").scrollLeft(); if (theClient.isIePre11()) { img.height = $(chrImg).outerHeight(); img.width = $(chrImg).outerWidth(); } else { img.height = $(chrImg).height(); img.width = $(chrImg).width(); } return img; } }); }; ////////////////////////// //// Drag Scroll code //// ////////////////////////// jQuery.fn.panImages = function(){ // globals across all panImages genomePos.original = genomePos.getOriginalPos(); // redundant but makes certain original is set. var leftLimit = hgTracks.imgBoxLeftLabel * -1; var rightLimit = (hgTracks.imgBoxPortalWidth - hgTracks.imgBoxWidth + leftLimit); var only1xScrolling = ((hgTracks.imgBoxWidth - hgTracks.imgBoxPortalWidth) === 0); var prevX = (hgTracks.imgBoxPortalOffsetX + hgTracks.imgBoxLeftLabel) * -1; var portalWidth = 0; var portalAbsoluteX = 0; var savedPosition; var highlightAreas = 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 || !pic) { throw "Not a div with 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').on("mousemove", function(e) { if (e.shiftKey) $(this).css('cursor',"crosshair"); // shift-dragZoom else if ( theClient.isIePre11() ) // IE will override map item cursors if this gets set $(this).css('cursor',""); // normal pointer when not over clickable item }); panAdjustHeight(prevX); pan.on("mousedown", function(e){ if (e.which > 1 || e.button > 1 || e.shiftKey || e.metaKey || e.altKey || e.ctrlKey) return true; if (mouseIsDown === false) { if (rightClick.menu) { rightClick.menu.hide(); } mouseIsDown = true; mouseDownX = e.clientX; highlightAreas = $('.highlightItem'); atEdge = (!beyondImage && (prevX >= leftLimit || prevX <= rightLimit)); $(document).on('mousemove',panner); $(document).on('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()) { // need to throw up a z-index div. Wait mask? savedPosition = genomePos.get(); dragMaskShow(); posting.blockMapClicks(); } var decelerator = 1; //var wingSize = 1000; // 0 stops the scroll at the edges. // Remeber that offsetX (prevX) is negative newX = prevX + relativeX; if ( newX >= leftLimit ) { // scrolled all the way to the left if (atEdge) { // Do not drag straight off edge. Force second drag beyondImage = true; newX = leftLimit + (newX - leftLimit)/decelerator;// slower //if (newX >= leftLimit + wingSize) // Don't go too far over the edge! // newX = leftLimit + wingSize; } else newX = leftLimit; } else if ( newX < rightLimit ) { // scrolled all the way to the right 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 posStatus = panUpdatePosition(newX,true); newX = posStatus.newX; // do not update highlights if we are at the end of a chromsome if (!posStatus.isOutsideChrom) scrollHighlight(relativeX); var nowPos = newX.toString() + "px"; $(".panImg").css( {'left': nowPos }); $('.tdData').css( {'backgroundPosition': nowPos } ); if (!only1xScrolling) panAdjustHeight(newX); // Will dynamically resize image while scrolling. } } } function panMouseUp(e) { // Must be a separate function instead of pan.mouseup event. //if (!e) e = window.event; if (mouseIsDown) { dragMaskClear(); $(document).off('mousemove',panner); $(document).off('mouseup',panMouseUp); mouseIsDown = false; // timeout incase the dragSelect.selectEnd was over a map item. select takes precedence. setTimeout(posting.allowMapClicks,50); // 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) genomePos.set(savedPosition); var oldPos = prevX.toString() + "px"; $(".panImg").css( {'left': oldPos }); $('.tdData').css( {'backgroundPosition': oldPos } ); if (highlightAreas) imageV2.drawHighlights(); return true; } // Do we need to fetch anything? if (beyondImage) { if (imageV2.inPlaceUpdate) { var pos = parsePosition(genomePos.get()); imageV2.navigateInPlace("db=" + getDb() + "&position=" + encodeURIComponent(pos.chrom + ":" + pos.start + "-" + pos.end), null, true); } else { document.TrackHeaderForm.submit(); } return true; // Make sure the setTimeout below is not called. } // Just a normal scroll within a >1X image if (prevX !== newX) { prevX = newX; if (!only1xScrolling) { //panAdjustHeight(newX); // Will resize image AFTER scrolling. // Important, since AJAX could lead to reinit after this within bounds scroll hgTracks.imgBoxPortalOffsetX = (prevX * -1) - hgTracks.imgBoxLeftLabel; hgTracks.imgBoxPortalLeft = newX.toString() + "px"; } } } } }); // end of this.each(function(){ function panUpdatePosition(newOffsetX,bounded) { // Updates the 'position/search" display with change due to panning var closedPortalStart = hgTracks.imgBoxPortalStart + 1; // Correction for half open var portalWidthBases = hgTracks.imgBoxPortalEnd - closedPortalStart; var portalScrolledX = hgTracks.imgBoxPortalOffsetX+hgTracks.imgBoxLeftLabel + newOffsetX; var recalculate = false; var newPortalStart = 0; if (hgTracks.revCmplDisp) newPortalStart = closedPortalStart + // As offset goes down, so do bases seen. Math.round(portalScrolledX*hgTracks.imgBoxBasesPerPixel); else newPortalStart = closedPortalStart - // As offset goes down, bases seen goes up! Math.round(portalScrolledX*hgTracks.imgBoxBasesPerPixel); if (newPortalStart < hgTracks.chromStart && bounded) { // Stay within bounds newPortalStart = hgTracks.chromStart; recalculate = true; } var newPortalEnd = newPortalStart + portalWidthBases; if (newPortalEnd > hgTracks.chromEnd && bounded) { newPortalEnd = hgTracks.chromEnd; newPortalStart = newPortalEnd - portalWidthBases; recalculate = true; } if (newPortalStart > 0) { var newPos = hgTracks.chromName + ":" + newPortalStart + "-" + newPortalEnd; genomePos.set(newPos); // no need to change the size } if (recalculate && hgTracks.imgBoxBasesPerPixel > 0) { // Need to recalculate X for bounding drag portalScrolledX = (closedPortalStart - newPortalStart) / hgTracks.imgBoxBasesPerPixel; newOffsetX = portalScrolledX - (hgTracks.imgBoxPortalOffsetX+hgTracks.imgBoxLeftLabel); } if (typeof (igv) !== "undefined") { igv.updateIgvStartPosition(newPortalStart); } ret = {}; ret.newX = newOffsetX; ret.isOutsideChrom = recalculate; return ret; } function mapTopAndBottom(mapName,east,west) { // Find the top and bottom px given left and right boundaries var mapPortal = { top: -10, bottom: -10 }; var items = $("map[name='"+mapName+"']").children(); if ($(items).length>0) { $(items).each(function(t) { var loc = this.coords.split(","); var aleft = parseInt(loc[0]); var aright = parseInt(loc[2]); if (aleft < west && aright >= east) { var atop = parseInt(loc[1]); var abottom = parseInt(loc[3]); if (mapPortal.top < 0 ) { mapPortal.top = atop; mapPortal.bottom = abottom; } else if (mapPortal.top > atop) { mapPortal.top = atop; } else if (mapPortal.bottom < abottom) { mapPortal.bottom = abottom; } } }); } return mapPortal; } function panAdjustHeight(newOffsetX) { // Adjust the height of the track data images so that bed items scrolled off screen // do not waste vertical real estate // Is the > 1x? if (only1xScrolling) return; var east = newOffsetX * -1; var west = east + portalWidth; $(".panImg").each(function(t) { var mapid = this.id.replace('img_','map_'); var hDiv = $(this).parent(); var north = parseInt($(this).css("top")) * -1; var south = north + $(hDiv).height(); var mapPortal = mapTopAndBottom(mapid,east,west); if (mapPortal.top > 0) { var topdif = Math.abs(mapPortal.top - north); var botdif = Math.abs(mapPortal.bottom - south); if (topdif > 2 || botdif > 2) { $(hDiv).height( mapPortal.bottom - mapPortal.top ); north = mapPortal.top * -1; $(this).css( {'top': north.toString() + "px" }); // Need to adjust side label height as well! var imgId = this.id.split("_"); var titlePx = 0; var center = $("#img_center_"+imgId[2]); if (center.length > 0) { titlePx = $(center).parent().height(); north += titlePx; } var side = $("#img_side_"+imgId[2]); if (side.length > 0) { $(side).parent().height( mapPortal.bottom - mapPortal.top + titlePx); $(side).css( {'top': north.toString() + "px" }); } var btn = $("#p_btn_"+imgId[2]); if (btn.length > 0) { $(btn).height( mapPortal.bottom - mapPortal.top + titlePx); } else { btn = $("#img_btn_"+imgId[2]); if (btn.length > 0) { $(btn).parent().height( mapPortal.bottom - mapPortal.top + titlePx); $(btn).css( {'top': top.toString() + "px" }); } } } } }); dragMaskResize(); // Resizes the dragMask to match current image size } function dragMaskShow() { // Sets up the dragMask to show grabbing cursor within image // and not allowed north and south of image var imgTbl = $('#imgTbl'); // Find or create the waitMask (which masks the whole page) var dragMask = normed($('div#dragMask')); if (!dragMask) { $("body").prepend("
"); dragMask = $('div#dragMask'); } $('body').css('cursor','not-allowed'); $(dragMask).css('cursor',"url(../images/grabbing.cur),w-resize"); $(dragMask).css({opacity:0.0,display:'block', top: $(imgTbl).position().top.toString() + 'px', height: $(imgTbl).height().toString() + 'px' }); } function dragMaskResize() { // Resizes dragMask (called when image is dynamically resized in >1x scrolling) var imgTbl = $('#imgTbl'); // Find or create the waitMask (which masks the whole page) var dragMask = normed($('div#dragMask')); if (dragMask) { $(dragMask).height( $(imgTbl).height() ); } } function dragMaskClear() { // Clears the dragMask $('body').css('cursor','auto'); var dragMask = normed($('#dragMask')); if (dragMask) $(dragMask).hide(); } function scrollHighlight(relativeX) // Scrolls the highlight region if one exists { if (highlightAreas) { for (i=0; i