bc236ffceb9ddc8632c872168c92c229206989af max Mon Aug 10 08:25:30 2026 -0700 hgc BLAT alignment viewer: consistent headings, section reorder, friendlier expired-link message, refs #37893 - Sidebar links and section headings are now all sentence case, and each sidebar link matches its heading (the Side-by-side heading is relabeled too). - Reorder the modern single-page view to Query, Side-by-side, Genome so the per-block jump links sit under Only genome sequence, where their anchors actually are, rather than under Side-by-side; tighten the block-link spacing. The reorder is done server-side (capture the shared library output via open_memstream, emit the sections in the new order) so the page does not reflow after it loads. - htcUserAli now shows a friendly 'no longer available, run a new BLAT search' message when the search's trash files have aged out, instead of a raw file-open error. - Free the open_memstream buffer with libc free(), not kent freeMem(). diff --git src/hg/hgc/hgc.c src/hg/hgc/hgc.c index 1f822180985..1e224043630 100644 --- src/hg/hgc/hgc.c +++ src/hg/hgc/hgc.c @@ -9092,31 +9092,31 @@ ".blatBtn{padding:4px 12px; font-size:13px; border:1px solid #999; border-radius:3px;" " background:#e6e6e6; text-decoration:none; white-space:nowrap; cursor:pointer}" /* nice_menu.css sets a:link blue (specificity 0,1,1); a.blatBtn:link (0,2,1) beats it */ "a.blatBtn:link, a.blatBtn:visited, a.blatBtn:hover{color:#000; text-decoration:none}" ".blatBtn:hover{background:#d8d8d8}" "#blatAlnBody{font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; color:#374a5e;" " display:grid; grid-template-columns:220px 1fr;" /* full-height sidebar + content column */ " background:#fff}" /* edge to edge: no margin, no border */ "#blatAlnBody a{color:#0a3a7a}" "#blatAlnBody a:hover{color:#8b1a1a}" "#blatAlnNav{grid-column:1; grid-row:1; background:#f4f7fb; border-right:1px solid #dde3ea}" /* keep the grey column full height, but pin the links so they stay visible while scrolling */ "#blatAlnNavInner{position:sticky; top:0; padding:18px 20px; display:flex; flex-direction:column;" " gap:16px}" "#blatAlnNav a{font-weight:700; text-decoration:none}" /* already obviously links; no underline */ - "#blatAlnBlocks{display:flex; flex-direction:column; gap:8px; margin:2px 0 0 14px}" /* block links, indented under side-by-side */ + "#blatAlnBlocks{display:flex; flex-direction:column; gap:2px; margin:2px 0 0 14px}" /* block links, tight list indented under genome sequence */ "#blatAlnBlocks a{font-weight:400; font-size:13px}" "#blatAlnContent{grid-column:2; grid-row:1; min-width:0; padding:0 20px 14px}" "#blatAlnContent h2{display:none}" "#blatAlnContent hr{display:none}" "#blatAlnContent h4{margin:16px -20px 0; padding:8px 20px; background:#4c759c; color:#fff;" " font-size:15px; font-weight:700}" /* -20px: bar spans full content width */ "#blatAlnContent h4:first-child{margin-top:0}" /* Alignment Summary flush at top */ "#blatAlnContent h4 a{color:#fff}" /* undo bootstrap.css (pulled in by webStartGbNoBanner's gbHeader) on the sequence blocks: * it would give <pre> a grey box, a border and word-break that mangles the alignment */ "#blatAlnContent pre{margin:0; padding:2px 0 12px; line-height:1.4; background:none; border:0;" " border-radius:0; color:#374a5e; white-space:pre; word-break:normal; word-wrap:normal}" /* key-value summary strip, mirroring hgBlat's .blatStrip (label over value, thin dividers) */ ".blatAlnStrip{display:flex; align-items:center; gap:24px; flex-wrap:wrap; margin:2px 0 14px}" ".blatAlnStat{display:flex; flex-direction:column; gap:1px}" @@ -9130,99 +9130,122 @@ printf("<div class='blatTitleBar'>"); printf("<span class='blatTtl'>BLAT Base Alignment: %s</span>", blatAsmLabel(database)); printf("<span class='blatBtns'>"); printf("<a href='hgBlat?blatReopen=1&hgsid=%s' class='blatBtn'>" "\xe2\x80\xb9 Back to results</a>", cartSessionId(cart)); if (canShare) printf("<a href='#' id='blatShareBtn' class='blatBtn'>Share a link</a>"); printf("</span></div>\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 */ printf("<div id='blatAlnBody'>\n"); printf("<div id='blatAlnContent'>\n"); -printf("<h4>Alignment Summary</h4>\n"); +printf("<h4>Alignment summary</h4>\n"); /* comma-format the coordinates and base counts, matching the new Table view (readable at the * hundreds-of-millions scale of genomic coordinates, and the convention elsewhere in the browser) */ char tStartC[32], tEndC[32], matchC[32], qSizeC[32]; sprintLongWithCommas(tStartC, psl->tStart + 1); sprintLongWithCommas(tEndC, psl->tEnd); sprintLongWithCommas(matchC, psl->match + psl->repMatch); sprintLongWithCommas(qSizeC, psl->qSize); /* key-value strip (Query / Position / Identity / Matches / Strand), styled like hgBlat's summary * strip so the two pages read as one design. */ printf("<div class='blatAlnStrip'>" "<div class='blatAlnStat'><span class='k'>Query</span><span class='v'>%s</span></div>" "<div class='d'></div>" "<div class='blatAlnStat'><span class='k'>Position</span><span class='v'>%s:%s-%s</span></div>" "<div class='d'></div>" "<div class='blatAlnStat'><span class='k'>Identity</span>" "<span class='v' style='color:%s'>%.1f%%</span></div>" "<div class='d'></div>" "<div class='blatAlnStat'><span class='k'>Matches</span><span class='v'>%s of %s</span></div>" "<div class='d'></div>" "<div class='blatAlnStat'><span class='k'>Strand</span><span class='v'>%s</span></div>" "</div>\n", qName, chrom, tStartC, tEndC, idColor, ident, matchC, qSizeC, psl->strand); if (isNotEmpty(aliasStr)) printf("<p>Genome sequence %s is also known as: %s.</p>\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; +/* Capture the shared library's alignment HTML so we can reorder its sections for this page. The + * library emits them as Query (#cDNA), Genome (#genomic), then Side-by-side (#ali), with the + * per-block anchors living inside the Genome section. We want Query, Side-by-side, Genome so the + * long per-block list sits at the bottom of both the page and the sidebar. Reorder here, on the + * server, rather than in JS, so the page does not reflow after it loads. */ +char *alnHtml = NULL; +size_t alnLen = 0; +FILE *alnF = open_memstream(&alnHtml, &alnLen); if (qType == gftRna || qType == gftDna) - blockCount = showPartialDnaAlignment(psl, oSeq, stdout, cdsS, cdsE, FALSE); + blockCount = showPartialDnaAlignment(psl, oSeq, alnF, cdsS, cdsE, FALSE); else - blockCount = showGfAlignment(psl, oSeq, stdout, qType, qStart, qEnd, qName); + blockCount = showGfAlignment(psl, oSeq, alnF, qType, qStart, qEnd, qName); +fclose(alnF); +char *pGenome = (alnHtml != NULL) ? stringIn("<H4><A NAME=genomic>", alnHtml) : NULL; +char *pAli = (alnHtml != NULL) ? stringIn("<H4><A NAME=ali>", alnHtml) : NULL; +if (pGenome != NULL && pAli != NULL && pGenome < pAli) + { /* Query, then Side-by-side, then Genome */ + fwrite(alnHtml, 1, pGenome - alnHtml, stdout); /* legend + Query (#cDNA) section */ + fputs(pAli, stdout); /* Side-by-side (#ali) section, through footnote */ + fwrite(pGenome, 1, pAli - pGenome, stdout); /* Genome (#genomic) section, with block anchors */ + } +else + fputs((alnHtml != NULL) ? alnHtml : "", stdout); +free(alnHtml); /* libc free: open_memstream's buffer is malloc'd, not a kent needMem block */ printf("</div>\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("<div id='blatAlnNav'><div id='blatAlnNavInner'>\n"); printf("<a href='#cDNA'>Only query sequence</a>\n" - "<a href='#genomic'>Only genome sequence</a>\n" - "<a href='#ali'>Side by Side Alignment</a>\n"); -if (blockCount > 1) /* per-block jump links, indented under the side-by-side item */ + "<a href='#ali'>Side by side alignment</a>\n" + "<a href='#genomic'>Only genome sequence</a>\n"); +if (blockCount > 1) /* per-block jump links, indented under the genome-sequence item where their anchors live */ { int bi; printf("<div id='blatAlnBlocks'>\n"); for (bi = 1; bi <= blockCount; ++bi) printf("<a href='#%d'>Block %d</a>\n", bi, bi); printf("</div>\n"); } printf("</div></div>\n"); printf("</div>\n"); /* #blatAlnBody */ -/* The cDNA/Genomic section headers come from shared library code (fuzzyShow.c / pslShow.c) as - * "cDNA <qName>" / "Genomic <chrom> :"; 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. */ +/* The cDNA/Genomic/Side-by-side section headers come from shared library code (fuzzyShow.c / + * pslShow.c) as "cDNA <qName>" / "Genomic <chrom> :" / "Side by Side Alignment"; relabel them to + * the sidebar wording (sentence case) via JS (there is no C hook for it), keeping the + * #cDNA/#genomic/#ali 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" + " var star = /\\*\\s*$/.test(h.textContent) ? '*' : '';\n" // keep the footnote marker if present " h.textContent = '';\n" " var k = document.createElement('a'); k.name = anchor; h.appendChild(k);\n" - " h.appendChild(document.createTextNode(text));\n" + " h.appendChild(document.createTextNode(text + star));\n" " }\n" "}\n" "relabel('cDNA', 'Only query sequence: %s');\n" "relabel('genomic', 'Only genome sequence: %s');\n" + "relabel('ali', 'Side by side alignment');\n" // match the sidebar wording and sentence case "})();\n", qName, chrom); /* "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 btn = document.getElementById('blatShareBtn');\n" "if (!btn) return;\n" "btn.addEventListener('click', function(ev){\n" " ev.preventDefault();\n" " if (btn.dataset.busy) return;\n" @@ -9260,30 +9283,37 @@ boolean modern = cartUsualBoolean(cart, "blatNewPage", FALSE); char title[1024]; safef(title, sizeof title, "User Sequence vs Genomic"); if (modern) { char pageTitle[256]; safef(pageTitle, sizeof pageTitle, "BLAT Base Alignment: %s", blatAsmLabel(database)); webStartGbNoBanner(cart, database, pageTitle); // menubar + <main>, no legacy section tables } else htmlFramesetStart(title); start = cartInt(cart, "o"); parseSs(fileNames, &pslName, &faName, &qName); +if (modern && (!fileExists(pslName) || !fileExists(faName))) + { /* the search's trash files have been cleaned up: a friendly note, not a raw file error */ + printf("<p>This BLAT alignment is no longer available. The search results it came from have " + "expired. Please run a new <a href=\"hgBlat\">BLAT search</a>.</p>\n"); + webEndGb(); + exit(0); + } 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))