c683ecb63d721deb02fa8ab15bf66f70f1c3a326 max Sat Jul 25 18:25:00 2026 -0700 hgBlat/hgc: single-page BLAT results view with shareable alignment links Add a modern single-page BLAT results table (hgBlat.js) and a non-frameset alignment view (showSomeAlignmentModern in hgc, gated by the blatNewPage cart var). Share/reopen a result set from a durable bigPsl custom track pinned in the cart via a saved session (htcBlatAlign / loadBlatShareSessionIfAny). Factor the shared helpers into a new blatShare module (lib/blatShare.c, inc/blatShare.h). diff --git src/hg/hgc/hgc.c src/hg/hgc/hgc.c index bdc8a98ff62..f4011c6fa18 100644 --- src/hg/hgc/hgc.c +++ src/hg/hgc/hgc.c @@ -237,30 +237,31 @@ #include "chromInfo.h" #include "gbWarn.h" #include "mammalPsg.h" #include "net.h" #include "jsHelper.h" #include "virusClick.h" #include "gwasCatalog.h" #include "mdb.h" #include "yaleGencodeAssoc.h" #include "itemDetailsHtml.h" #include "trackVersion.h" #include "numtsClick.h" #include "geneReviewsClick.h" #include "bigBed.h" #include "bigPsl.h" +#include "blatShare.h" #include "bedTabix.h" #include "longRange.h" #include "hmmstats.h" #include "aveStats.h" #include "trix.h" #include "bPlusTree.h" #include "customFactory.h" #include "dupTrack.h" #include "iupac.h" #include "clinvarSubLolly.h" #include "jsHelper.h" #include "errCatch.h" #include "htslib/bgzf.h" #include "htslib/kstring.h" #include "pipeline.h" @@ -8219,30 +8220,31 @@ if ((psl->strand[1] == '+') && (qType == gftProt)) tEnd = psl->tStarts[psl->blockCount - 1] + psl->blockSizes[psl->blockCount - 1] * 3; tSeq = hDnaFromSeq(database, seqName, tStart, tEnd, dnaLower); freez(&tSeq->name); tSeq->name = cloneString(psl->tName); safef(tName, sizeof(tName), "%s.%s", organism, psl->tName); if (qName == NULL) fprintf(f, "

Alignment of %s and %s:%d-%d

\n", psl->qName, psl->tName, psl->tStart+1, psl->tEnd); else fprintf(f, "

Alignment of %s and %s:%d-%d

\n", qName, psl->tName, psl->tStart+1, psl->tEnd); +if (!cartUsualBoolean(cart, "blatNewPage", FALSE)) /* no "frame" in the new single-page view */ fputs("Click on links in the frame to the left to navigate through " "the alignment.\n", f); blockCount = pslShowAlignment(psl, qType == gftProt, qName, qSeq, qStart, qEnd, tName, tSeq, tStart, tEnd, f); freeDnaSeq(&tSeq); return blockCount; } static struct ffAli *pslToFfAliAndSequence(struct psl *psl, struct dnaSeq *qSeq, boolean *retIsRc, struct dnaSeq **retSeq, int *retTStart) /* Given psl, dig up target sequence and convert to ffAli. * Note: if strand is -, this does a pslRc to psl! */ { @@ -8334,30 +8336,31 @@ if (restrictToWindow) { /* Find start/end in ffAli, which is clipped to target from PSL and maybe RC'd */ ffStartEndQ(rna, ffAli, &rnaStart, &rnaEnd); /* Add 100 bases either side if possible */ if (rnaStart >= 100) rnaStart -= 100; if (rnaEnd + 100 <= rnaSize) rnaEnd += 100; } /* Write body heading info. */ char *displayChromName = chromAliasGetDisplayChrom(database, cart, psl->tName); fprintf(body, "

Alignment of %s and %s:%d-%d

\n", psl->qName, displayChromName, partTStart+1, partTEnd); +if (!cartUsualBoolean(cart, "blatNewPage", FALSE)) /* no "frame" in the new single-page view */ fprintf(body, "Click on links in the frame to the left to navigate through " "the alignment.\n"); blockCount = ffShAliPart(body, ffAli, wholePsl->qName, rna + rnaStart, rnaEnd - rnaStart, rnaStart, displayChromName, dnaSeq->dna, dnaSeq->size, wholeTStart, 8, FALSE, isRc, FALSE, TRUE, TRUE, TRUE, TRUE, cdsS, cdsE, partTStart, partTEnd); return blockCount; } void showSomeAlignment(struct psl *psl, bioSeq *oSeq, enum gfType qType, int qStart, int qEnd, char *qName, int cdsS, int cdsE) @@ -8996,66 +8999,277 @@ safef(name, sizeof name, "%s", psl->qName); } else { qSeq = loadGenomePart(otherDb, psl->qName, psl->qStart, psl->qEnd); safef(name, sizeof name, "%s.%s", otherOrg, psl->qName); } char title[1024]; safef(title, sizeof title, "%s %s vs %s %s ", (otherOrg == NULL ? "" : otherOrg), psl->qName, org, psl->tName ); htmlFramesetStart(title); /*showSomeAlignment(psl, qSeq, gftDnaX, psl->qStart, psl->qEnd, name, 0, 0); */ showSomeAlignment(psl, qSeq, gftDnaX, psl->qStart, psl->qEnd, name, cdsStart, cdsEnd); } +static void showSomeAlignmentModern(struct psl *psl, bioSeq *oSeq, enum gfType qType, + int qStart, int qEnd, char *qName, int cdsS, int cdsE) +/* Modern single-page version of showSomeAlignment for hgBlat's new table mode: a one-line summary, + * a back link and three "jump to" links, then the base-by-base alignment inlined below with + * steel-blue section headers - all in one white panel, so the whole page scrolls (no ). + * The alignment body itself is generated exactly as before. The caller supplies the page chrome + * via cartWebStart(). */ +{ +if (qName == NULL) + qName = psl->qName; +char *chrom = chromAliasGetDisplayChrom(database, cart, psl->tName); +double ident = 100.0 - pslCalcMilliBad(psl, TRUE) * 0.1; + +char *idColor = (ident >= 98) ? "#1f7a34" : (ident >= 95) ? "#4d7c0f" : + (ident >= 90) ? "#b45309" : "#b1301f"; + +/* Offer "Share a link" only when a durable bigPsl custom track backs these results; without it there + * is nothing for a shared session to rebuild the alignment from. */ +char *shareBb = blatFindPinnedBigPsl(cart); +boolean canShare = (shareBb != NULL); +freeMem(shareBb); + +/* Colors imported from the BLAT Redesign (slide 3): grey page, steel-blue section-header bars, + * navy links with maroon hover, slate text. The

/
the shared alignment code emits are + * hidden; its

section headings become the steel-blue bars. */ +printf("\n"); + +/* one white panel laid out as two grid columns: a full-height "jump to" sidebar on the left, and on + * the right an "Alignment Summary" header, the summary line, and the base-by-base alignment inlined + * so the whole page scrolls */ +if (canShare) + printf("\n"); + +printf("
\n"); + +printf("
\n"); +printf("Only query sequence\n" + "Only genome sequence\n" + "Side-by-side alignment\n"); +if (psl->blockCount > 1) /* per-block jump links, indented under the side-by-side item */ + { + int bi; + printf("
\n"); + for (bi = 1; bi <= psl->blockCount; ++bi) + printf("Block %d\n", bi, bi); + printf("
\n"); + } +printf("
\n"); + +printf("
\n"); +printf("

Alignment Summary

\n"); +printf("

%s aligned to %s:%d-%d, " + "%.1f%% identity, " + "%d of %d bases matched, strand %s.

\n", + qName, chrom, psl->tStart + 1, psl->tEnd, idColor, ident, + psl->match + psl->repMatch, psl->qSize, psl->strand); +if (qType == gftRna || qType == gftDna) + showPartialDnaAlignment(psl, oSeq, stdout, cdsS, cdsE, FALSE); +else + showGfAlignment(psl, oSeq, stdout, qType, qStart, qEnd, qName); +printf("
\n"); /* #blatAlnContent */ + +printf("
\n"); /* #blatAlnBody */ + +/* Two touch-ups that need the rendered DOM: (1) the cDNA/Genomic section headers come from shared + * library code (fuzzyShow.c / pslShow.c) as "cDNA " / "Genomic :" - relabel them to + * match the sidebar wording, keeping the #cDNA/#genomic jump anchors; (2) add a right-aligned "Back + * to results" button into the gold title band. qName and chrom are already sanitized. */ +jsInlineF( + "(function(){\n" + "function relabel(anchor, text){\n" + " var a = document.getElementsByName(anchor);\n" + " if (a && a.length){\n" + " var h = a[0].parentNode;\n" + " h.textContent = '';\n" + " var k = document.createElement('a'); k.name = anchor; h.appendChild(k);\n" + " h.appendChild(document.createTextNode(text));\n" + " }\n" + "}\n" + "relabel('cDNA', 'Only query sequence: %s');\n" + "relabel('genomic', 'Only genome sequence: %s');\n" + "var t = document.getElementById('sectTtl');\n" + "if (t) t.textContent = 'BLAT Base Alignment: %s';\n" + "var bar = document.querySelector('.subheadingBar');\n" + "if (bar){\n" + " var b = document.createElement('a');\n" + " b.id = 'blatBackBtn';\n" + " b.href = 'hgBlat?blatReopen=1&hgsid=%s';\n" + " b.textContent = '\\u2039 Back to results';\n" + " b.style.cssText = 'padding:5px 15px; background:#0a2b6b; color:#fff; font-weight:700;" + " font-size:13px; border-radius:3px; text-decoration:none; white-space:nowrap';\n" + " bar.appendChild(b);\n" + "}\n" + "})();\n", + qName, chrom, database, cartSessionId(cart)); + +/* "Share a link" button in the gold title band: save an anonymous session (hgSession API), then + * build a durable hgc?g=htcBlatAlign link that rebuilds THIS alignment from the session's durable + * bigPsl custom track (no BLAT re-run, no stored trash sequence). Mirrors hgBlat.js blatShareLink(); + * qName is already sanitized. */ +if (canShare) + jsInlineF( + "(function(){\n" + "var bar = document.querySelector('.subheadingBar');\n" + "if (!bar) return;\n" + "var btn = document.createElement('a');\n" + "btn.href = '#';\n" + "btn.textContent = 'Share a link';\n" + "btn.style.cssText = 'padding:5px 15px; background:#fff; color:#0a2b6b; border:1px solid #0a2b6b;" + " font-weight:700; font-size:13px; border-radius:3px; text-decoration:none; white-space:nowrap;" + " cursor:pointer';\n" + "var backBtn = document.getElementById('blatBackBtn');\n" + "if (backBtn) bar.insertBefore(btn, backBtn); else bar.appendChild(btn);\n" + "var box = document.getElementById('blatAlnShareBox');\n" + "btn.addEventListener('click', function(ev){\n" + " ev.preventDefault();\n" + " if (!box) return;\n" + " box.style.display = 'block';\n" + " box.textContent = 'Creating shareable link\\u2026';\n" + " fetch('../cgi-bin/hgSession', {method:'POST', credentials:'same-origin'," + " headers:{'Content-Type':'application/x-www-form-urlencoded'}," + " body:'hgsid=%s&hgS_doSaveSessionJson=1&hgS_shareAnon=1'})\n" + " .then(function(r){ return r.json(); }).then(function(data){\n" + " if (!data || !data.name){ box.textContent = 'Could not create link.'; return; }\n" + " var link = window.location.origin + window.location.pathname +\n" + " '?g=htcBlatAlign&db=%s&c=%s&o=%d&l=%d&r=%d&i=' + encodeURIComponent('%s') +\n" + " '&u=l&s=' + encodeURIComponent(data.name);\n" + " box.innerHTML = '';\n" + " var msg = document.createElement('div');\n" + " msg.textContent = 'Shareable link (opens this alignment for anyone):';\n" + " msg.style.marginBottom = '6px';\n" + " var inp = document.createElement('input');\n" + " inp.type = 'text'; inp.readOnly = true; inp.value = link;\n" + " inp.style.cssText = 'width:70%%; max-width:640px; font-size:13px; padding:5px 8px;" + " border:1px solid #c4cdd6; border-radius:3px';\n" + " var cp = document.createElement('button');\n" + " cp.type = 'button'; cp.textContent = 'Copy';\n" + " cp.style.cssText = 'margin-left:8px; padding:5px 12px; font-size:13px;" + " border:1px solid #0a2b6b; background:#0a2b6b; color:#fff; border-radius:3px; cursor:pointer';\n" + " cp.addEventListener('click', function(){ inp.select();" + " if (navigator.clipboard){ navigator.clipboard.writeText(link); }" + " else { document.execCommand('copy'); } cp.textContent = 'Copied'; });\n" + " box.appendChild(msg); box.appendChild(inp); box.appendChild(cp);\n" + " }).catch(function(){ box.textContent = 'Could not reach the server. Please try again.'; });\n" + "});\n" + "})();\n", + cartSessionId(cart), database, psl->tName, psl->tStart, psl->tStart, psl->tEnd, qName); +} + void htcUserAli(char *fileNames) /* Show alignment for accession. */ { char *pslName, *faName, *qName; struct lineFile *lf; bioSeq *oSeqList = NULL, *oSeq = NULL; struct psl *psl; int start; enum gfType tt, qt; boolean isProt; +/* In hgBlat's new table mode (blatNewPage) show a modern single-page alignment instead of the + * classic two-frame . */ +boolean modern = cartUsualBoolean(cart, "blatNewPage", FALSE); char title[1024]; safef(title, sizeof title, "User Sequence vs Genomic"); +if (modern) + cartWebStart(cart, database, "BLAT Base Alignment"); // full page chrome: menubar + sans-serif +else htmlFramesetStart(title); start = cartInt(cart, "o"); parseSs(fileNames, &pslName, &faName, &qName); pslxFileOpen(pslName, &qt, &tt, &lf); isProt = (qt == gftProt); while ((psl = pslNext(lf)) != NULL) { if (sameString(psl->tName, seqName) && psl->tStart == start && sameString(psl->qName, qName)) break; pslFree(&psl); } lineFileClose(&lf); if (psl == NULL) errAbort("Couldn't find alignment at %s:%d", seqName, start); oSeqList = faReadAllSeq(faName, !isProt); for (oSeq = oSeqList; oSeq != NULL; oSeq = oSeq->next) { if (sameString(oSeq->name, qName)) break; } if (oSeq == NULL) errAbort("%s is in %s but not in %s. Internal error.", qName, pslName, faName); -showSomeAlignment(psl, oSeq, qt, 0, oSeq->size, NULL, 0, 0); +if (modern) + showSomeAlignmentModern(psl, oSeq, qt, 0, oSeq->size, NULL, 0, 0); // cartWebStart page; framework closes it +else + showSomeAlignment(psl, oSeq, qt, 0, oSeq->size, NULL, 0, 0); // classic frameset; exits itself +} + +void htcBlatAlign(char *qName) +/* Durable base-by-base alignment for a shared BLAT link (g=htcBlatAlign): rebuild one alignment from + * the saved session's durable bigPsl custom track (blatLastBigBed) instead of the ephemeral trash + * .pslx/.fa the fresh-search htcUserAli path reads. seqName and o identify the hit; the query + * sequence comes from the bigPsl record itself, so no stored trash sequence is needed. This backs + * the "Share a link" button on the modern alignment page. */ +{ +cartWebStart(cart, database, "BLAT Base Alignment"); // full page chrome: menubar + sans-serif +char *bbFile = blatFindPinnedBigPsl(cart); +if (bbFile == NULL || !fileExists(bbFile)) + { + printf("

This shared BLAT alignment is no longer available. The custom track that stored it " + "has expired or been removed. Please run a new BLAT search.

\n"); + return; + } +int start = cartInt(cart, "o"); +char *seq = NULL; +struct psl *psl = pslFromBigPslFileMatch(bbFile, seqName, start, qName, &seq, NULL); +if (psl == NULL || seq == NULL) + { + printf("

This alignment was not found in the shared BLAT results.

\n"); + return; + } +enum gfType qType = pslIsProtein(psl) ? gftProt : gftDna; +struct dnaSeq *oSeq = newDnaSeq(cloneString(seq), strlen(seq), qName); +showSomeAlignmentModern(psl, oSeq, qType, 0, oSeq->size, NULL, 0, 0); // cartWebStart page; framework closes it } void htcProteinAli(char *readName, char *table) /* Show protein to translated dna alignment for accession. */ { struct psl *psl; int start; enum gfType qt = gftProt; struct sqlResult *sr; struct sqlConnection *conn = hAllocConn(database); struct dnaSeq *seq = NULL; char query[256], **row; char fullTable[HDB_MAX_TABLE_STRING]; boolean hasBin; char buffer[256]; @@ -27126,61 +27340,98 @@ boolean isProt = cgiOptionalString("isProt") != NULL; char *customTextTemplate = "track type=bigPsl indelDoubleInsert=on indelQueryInsert=on pslFile=%s visibility=pack showAll=on htmlUrl=http://%s/goldenPath/help/hgUserPsl.html %s bigDataUrl=%s name=\"%s\" description=\"%s\" colorByStrand=\"0,0,0 0,0,150\" mouseOver=\"${oChromStart}-${oChromEnd} of ${oChromSize} bp, strand ${oStrand}\"\n"; char *extraForMismatch = "indelPolyA=on showDiffBasesAllScales=. baseColorUseSequence=lfExtra baseColorDefault=diffBases"; if (isProt) extraForMismatch = ""; char buffer[4096]; safef(buffer, sizeof buffer, customTextTemplate, bigBedTn.forCgi, host, extraForMismatch, bigBedTn.forCgi, trackName, trackDescription); struct customTrack *ctList = getCtList(); struct customTrack *newCts = customFactoryParse(database, buffer, FALSE, NULL, NULL); theCtList = customTrackAddToList(ctList, newCts, NULL, FALSE); customTracksSaveCart(database, cart, theCtList); +/* Pin this bigPsl file in the cart so hgBlat's Table view can reopen exactly these results from a + * shared session (see doShareReopen in hgBlat.c) - unambiguously, even when the cart holds several + * BLAT custom tracks from earlier searches. */ +cartSetString(cart, "blatLastBigBed", bigBedFile); + cartSetString(cart, "i", "PrintAllSequences"); hgCustom(newCts->tdb->track, NULL); if (sameOk(cartOptionalString(cart, "autoRearr"), "1")) { char snakeVar[256]; safef(snakeVar, sizeof snakeVar, "%s.doSnake", newCts->tdb->track); cartSetString(cart, snakeVar, "1"); } } void doHPRCTable(struct trackDb *tdb, char *itemName) /* Put up a generic bigBed details page, with a table of links to turn on related * * chain tracks with visibility toggles */ { int start = cartInt(cart, "o"); int end = cartInt(cart, "t"); genericHeader(tdb, itemName); genericBigBedClick(NULL, tdb, itemName, start, end, 0); printTrackHtml(tdb); // tell the javscript to reorganize the column of assemblies: jsIncludeFile("hgc.js", NULL); jsInlineF("var doHPRCTable = true;\n"); } boolean findNameBasedHandler(struct trackDb *tdb, char *track, char *item); +static void loadBlatShareSessionIfAny() +/* A durable BLAT "Share a link" alignment (hgc?g=htcBlatAlign&u=l&s=NAME&...) rebuilds one alignment + * from a saved anonymous session's durable bigPsl custom track. Load that session so the cart gets + * its blatLastBigBed and custom track, then restore this link's own db/position/track/item, which + * identify the specific alignment rather than the session's saved browser view. */ +{ +if (cgiOptionalString("s") == NULL || !sameOk(cgiOptionalString("g"), "htcBlatAlign")) + return; +/* The whole-cart session load can overwrite these with the session's saved values; remember the + * link's own copies and put them back afterwards. */ +char *keep[] = {"g", "db", "c", "o", "t", "l", "r", "i"}; +struct hash *saved = hashNew(0); +int i; +for (i = 0; i < ArraySize(keep); ++i) + { + char *v = cgiOptionalString(keep[i]); + if (v != NULL) + hashAdd(saved, keep[i], cloneString(v)); + } +struct sqlConnection *sConn = hConnectCentral(); +cartLoadUserSession(sConn, cgiUsualString("u", "l"), cgiString("s"), cart, NULL, NULL); +hDisconnectCentral(&sConn); +for (i = 0; i < ArraySize(keep); ++i) + { + char *v = hashFindVal(saved, keep[i]); + if (v != NULL) + cartSetString(cart, keep[i], v); + } +hashFree(&saved); +} + void doMiddle() /* Generate body of HTML. */ { +loadBlatShareSessionIfAny(); char *track = cartString(cart, "g"); char *item = cloneString(cartOptionalString(cart, "i")); char *parentWigMaf = cartOptionalString(cart, "parentWigMaf"); struct trackDb *tdb = NULL; char *dupWholeName = NULL; boolean isDup = isDupTrack(track); if (isDup) { dupWholeName = track; track = dupTrackSkipToSourceName(track); } if (issueBotWarning) { @@ -28032,30 +28283,34 @@ { htcBigPslAliInWindow(item); } else if (sameWord(table, "htcBigPslAli")) { htcBigPslAli(item); } else if (sameWord(table, "htcCdnaAli")) { htcCdnaAli(item); } else if (sameWord(table, "htcUserAli")) { htcUserAli(item); } +else if (sameWord(table, "htcBlatAlign")) + { + htcBlatAlign(item); + } else if (sameWord(table, "htcGetBlastPep")) { doGetBlastPep(item, cartString(cart, "aliTable")); } else if (sameWord(table, "htcProteinAli")) { htcProteinAli(item, cartString(cart, "aliTable")); } else if (sameWord(table, "htcBlatXeno")) { htcBlatXeno(item, cartString(cart, "aliTable")); } else if (sameWord(table, "htcExtSeq")) { htcExtSeq(item);