6ae582f5103a8b27420c44a8f92f210969f65f4c max Sat Jul 25 21:36:18 2026 -0700 hgc BLAT alignment viewer: table-free page, share dialog, chromAlias, block-count fix refs #37893 Modern base-alignment page (showSomeAlignmentModern): - Switch page chrome to webStartGbNoBanner()/webEndGb() (menubar +
, no legacy nested section tables) and draw the gold title bar + grey Back/Share buttons in plain HTML instead of injecting them with JS. - Full-height "jump to" sidebar; its links are position:sticky so they stay visible while scrolling the long alignment. The per-block "Block N" links use the block count the renderer returns, not psl->blockCount. The DNA path (ffShAliPart) merges blocks separated by gaps <= 8 bases, so raw psl->blockCount over-counted and produced links to #N anchors that did not exist; the returned count matches the emitted anchors. - Show the genomic sequence's chromAlias names ("Genome sequence chr7 is also known as: ..."), de-duplicated. Friendly assembly label (organism + accession) for hub databases instead of the internal hub_NNN_ name. Durable "Share a link": - Opens the shared topLinks.js "Share a link" modal and hands it a durable hgc?g=htcBlatAlign link. The link carries only the hit selectors (c/o/i) plus u=l&s=; db and the browser window come from the saved session. htcBlatAlign rebuilds the one alignment from the session's durable bigPsl custom track (no BLAT re-run). - Restore the per-hit "Alignment" links on shared-session results tables (hgBlat.c). lib/cart.c: - cartWriteHeaderAndCont() is now idempotent (guards on cartDidContentType). hgc writes the CGI header early, so a later webStartGb* would otherwise write a second Set-Cookie/Content-Type into the page body. diff --git src/hg/hgc/hgc.c src/hg/hgc/hgc.c index f4011c6fa18..1176e8aafed 100644 --- src/hg/hgc/hgc.c +++ src/hg/hgc/hgc.c @@ -8999,277 +8999,320 @@ 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 char *blatAsmLabel(char *database) +/* A user-facing assembly label for the page title. For an assembly hub the internal + * "hub_NNN_GCA_..." database name is not helpful, so use the assembly's friendly organism plus its + * accession; for a native assembly just use the db name (e.g. "hg38"). */ +{ +if (!trackHubDatabase(database)) + return cloneString(database); +char *acc = trackHubSkipHubName(database); /* drop the "hub_NNN_" prefix -> the accession */ +char *org = hGenome(acc); /* GenArk table's friendly genome name for GC* accs */ +if (isEmpty(org)) + org = trackHubAssemblyField(database, "organism"); /* else the hub's genomes.txt organism */ +if (isEmpty(org)) + return cloneString(acc); +char buf[256]; +safef(buf, sizeof buf, "%s %s", org, acc); +return cloneString(buf); +} + 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(). */ +/* Modern single-page version of showSomeAlignment for hgBlat's new table mode: a gold title bar with + * Back/Share buttons, a full-height "jump to" sidebar, then an "Alignment Summary" and the base-by- + * base alignment inlined below with steel-blue section headers, so the whole page scrolls (no + * ). The alignment body itself is generated by the shared library as before. The caller + * supplies the page chrome via webStartGbNoBanner()/webEndGb() - a menubar and
with no legacy + * section tables - so everything here is plain, table-free HTML. */ { if (qName == NULL) qName = psl->qName; char *chrom = chromAliasGetDisplayChrom(database, cart, psl->tName); +/* Alternate (chromAlias) names for the genomic sequence - e.g. its RefSeq/GenBank/Ensembl accessions + * - shown after the main name in the "Only genome sequence" header. */ +struct dyString *aliasDy = dyStringNew(128); +struct slName *aliasList = chromAliasFindAliases(psl->tName), *al; +struct hash *seenAlias = hashNew(0); +hashStore(seenAlias, chrom); /* skip the name already shown, and the native name */ +hashStore(seenAlias, psl->tName); +boolean firstAlias = TRUE; +for (al = aliasList; al != NULL; al = al->next) + { + if (isEmpty(al->name) || hashLookup(seenAlias, al->name)) + continue; /* skip empties, the shown name, and duplicates (e.g. Ensembl and GenBank "7") */ + hashStore(seenAlias, al->name); + dyStringPrintf(aliasDy, "%s%s", firstAlias ? "" : ", ", al->name); + firstAlias = FALSE; + } +hashFree(&seenAlias); +char *aliasStr = dyStringCannibalize(&aliasDy); /* "NC_000007.14, CM000669.2, 7" or "" */ 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. */ +/* 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. The page chrome is webStartGbNoBanner (a menubar + * and
, no legacy section tables), so we draw our own gold title bar in plain HTML. */ printf("\n"); +/* gold title bar, drawn directly (no framework subheadingBar, no JS): title on the left, then a + * "Share a link" button (when a durable track backs the results) and a "Back to results" button. */ +printf("
"); +printf("BLAT Base Alignment: %s", blatAsmLabel(database)); +printf(""); +printf("" + "\xe2\x80\xb9 Back to results", cartSessionId(cart)); +if (canShare) + printf("Share a link"); +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("
\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 (isNotEmpty(aliasStr)) + printf("

Genome sequence %s is also known as: %s.

\n", chrom, aliasStr); +/* The shared library returns the number of alignment blocks it actually shows. The DNA path merges + * blocks separated by gaps <= 8 bases, so this can be fewer than psl->blockCount; use it (not + * psl->blockCount) so the sidebar's "Block N" links match the #1..#N anchors that were emitted. */ +int blockCount; if (qType == gftRna || qType == gftDna) - showPartialDnaAlignment(psl, oSeq, stdout, cdsS, cdsE, FALSE); + blockCount = showPartialDnaAlignment(psl, oSeq, stdout, cdsS, cdsE, FALSE); else - showGfAlignment(psl, oSeq, stdout, qType, qStart, qEnd, qName); + blockCount = showGfAlignment(psl, oSeq, stdout, qType, qStart, qEnd, qName); printf("
\n"); /* #blatAlnContent */ +/* Sidebar, emitted after the alignment so blockCount is known; CSS grid puts it back in column 1. + * The inner div is position:sticky so the links stay in view as the long alignment scrolls. */ +printf("
\n"); +printf("Only query sequence\n" + "Only genome sequence\n" + "Side-by-side alignment\n"); +if (blockCount > 1) /* per-block jump links, indented under the side-by-side item */ + { + int bi; + printf("
\n"); + for (bi = 1; bi <= blockCount; ++bi) + printf("Block %d\n", bi, bi); + printf("
\n"); + } +printf("
\n"); + 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. */ +/* The cDNA/Genomic section headers come from shared library code (fuzzyShow.c / pslShow.c) as + * "cDNA " / "Genomic :"; relabel them to the sidebar wording via JS (there is no C + * hook for it), keeping the #cDNA/#genomic jump anchors. 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)); + qName, chrom); -/* "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. */ +/* "Share a link": save an anonymous session (hgSession API), 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), and hand it to the shared "Share a link" modal (topLinks.js shareUrl, + * loaded by the menu bar) so it looks like every other share dialog. 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" + "var btn = document.getElementById('blatShareBtn');\n" + "if (!btn) return;\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" + " if (btn.dataset.busy) return;\n" + " btn.dataset.busy = '1';\n" + " var label = btn.textContent;\n" + " btn.textContent = 'Creating 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" + " btn.textContent = label; btn.dataset.busy = '';\n" + " if (!data || !data.name) 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" + " '?g=htcBlatAlign&c=%s&o=%d&i=' + encodeURIComponent('%s') +\n" // db comes from the session " '&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" + " if (window.topLinks && topLinks.shareUrl) topLinks.shareUrl(link);\n" + " }).catch(function(){ btn.textContent = label; btn.dataset.busy = ''; });\n" "});\n" "})();\n", - cartSessionId(cart), database, psl->tName, psl->tStart, psl->tStart, psl->tEnd, qName); + cartSessionId(cart), psl->tName, psl->tStart, 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 + { + char pageTitle[256]; + safef(pageTitle, sizeof pageTitle, "BLAT Base Alignment: %s", blatAsmLabel(database)); + webStartGbNoBanner(cart, database, pageTitle); // menubar +
, no legacy section tables + } 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); if (modern) - showSomeAlignmentModern(psl, oSeq, qt, 0, oSeq->size, NULL, 0, 0); // cartWebStart page; framework closes it + { + showSomeAlignmentModern(psl, oSeq, qt, 0, oSeq->size, NULL, 0, 0); + webEndGb(); + exit(0); // we drew the whole page; skip the framework's table-closing cartHtmlEnd + } 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 pageTitle[256]; +safef(pageTitle, sizeof pageTitle, "BLAT Base Alignment: %s", database); +webStartGbNoBanner(cart, database, pageTitle); // menubar +
, no legacy section tables 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; + webEndGb(); + exit(0); } 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; + webEndGb(); + exit(0); } 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 +showSomeAlignmentModern(psl, oSeq, qType, 0, oSeq->size, NULL, 0, 0); +webEndGb(); +exit(0); // we drew the whole page; skip the framework's table-closing cartHtmlEnd } 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]; @@ -27376,38 +27419,40 @@ 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. */ + * its db, blatLastBigBed and custom track, then restore the link's own hit selectors, which identify + * which single alignment to show rather than the session's saved browser view. Only g/c/o/i are + * selectors (the handler, chrom, start and query name); db and the browser window come from the + * session, so the shared link needs to carry only those four. */ { 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"}; +/* The whole-cart session load frees the cart's current values; remember the link's own selectors and + * put them back afterwards (g and i are also excluded from saved sessions, so they must come here). */ +char *keep[] = {"g", "c", "o", "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)