595531f894a27e59e18b32436f6cb932fa4374a0
lrnassar
  Tue Sep 8 13:03:44 2026 -0700
BLAT form character counter: per-type limits passed through from the C constants. refs #38293

The counter showed the 75,000 DNA limit for every query type; protein and translated queries
are capped at 10,000, so an oversized protein paste looked fine until the server rejected it.
The per-sequence limits are now named constants in hgBlat.c, emitted into hgBlatFormData and
read by the counter, which keys on the Query type dropdown and recounts when it changes -
so the numbers cannot drift apart again. BLAT's guess counts against the DNA limit.

diff --git src/hg/js/hgBlat.js src/hg/js/hgBlat.js
index e636243b878..178b986220d 100644
--- src/hg/js/hgBlat.js
+++ src/hg/js/hgBlat.js
@@ -690,35 +690,42 @@
 }
 
 function blatOpts(list, cur) {
     return list.map(function(v) {
         return `<option value="${htmlEncode(v)}"${v === cur ? ' selected' : ''}>${htmlEncode(v)}</option>`;
     }).join('');
 }
 
 function blatFormCount() {
     // Live character count under the textarea.  Only these two nodes are touched on input - the
     // textarea itself is never re-rendered, so the caret stays where the user put it.
     var ta = document.getElementById('blatUserSeq');
     var out = document.getElementById('blatCountText');
     if (!ta || !out) { return; }
     var n = ta.value.replace(/[^A-Za-z*]/g, '').length;
-    // 75,000 is the DNA per-sequence limit (hgBlat.c maxSingleSize); protein and translated
-    // queries top out at 10,000, but the server rejects those with a warning that links to the
-    // full limits in the docs, so the counter shows only the common case.
-    out.textContent = blatFmt(n) + ' of 75,000 characters';
-    out.classList.toggle('over', n > 75000);
+    // The per-sequence limit for the selected query type, passed through from the C constants in
+    // hgBlat.c so the two cannot drift apart (the page claimed a stale 25,000 for years).
+    // "BLAT's guess" counts against the DNA limit - the common case; a protein query that only
+    // the server recognizes as such is still rejected there with a warning linking the full
+    // limits in the docs.
+    var cfg = hgBlatFormData;
+    var typeSel = document.querySelector('#blatFormBox select[name=type]');
+    var type = typeSel ? typeSel.value : '';
+    var isTx = (type === 'protein' || type === 'translated RNA' || type === 'translated DNA');
+    var max = isTx ? cfg.maxSingleTx : cfg.maxSingleDna;
+    out.textContent = blatFmt(n) + ' of ' + blatFmt(max) + ' characters';
+    out.classList.toggle('over', n > max);
 }
 
 function blatFormTab(showUpload) {
     $('#blatTabPaste').toggleClass('on', !showUpload);
     $('#blatTabUpload').toggleClass('on', showUpload);
     $('#blatPanePaste').toggle(!showUpload);
     $('#blatPaneUpload').toggle(showUpload);
 }
 
 function blatFormBusyMarkup() {
     // Spinner overlay shown between Submit and the arrival of the results page.  Built with the
     // form (hidden) rather than on demand, so nothing has to be parsed or fetched at the moment the
     // browser is already busy navigating away.
     return '<div id="blatBusyBg" class="gbBusyBg" role="status" aria-live="polite">' +
         '<div class="gbBusyCard"><div class="gbSpinner"></div>' +
@@ -988,30 +995,32 @@
     if (holder) { document.getElementById('blatGenomeSlot').appendChild(holder); }
 
 
     // Show the current assembly in the search bar itself instead of in a separate "Current genome:"
     // line - the bar is wide enough for the whole description.  setupGenomeSearchBar writes the new
     // one in on each pick, and focusing the bar selects all of it, so it reads as a filled-in search
     // box rather than as a value the user has to clear by hand.
     var genomeInput = document.getElementById('genomeSearch');
     if (genomeInput && cfg.dbLabel) { genomeInput.value = cfg.dbLabel; }
 
     // Restore the sequence from the cart without going through innerHTML (avoids re-escaping).
     document.getElementById('blatUserSeq').value = cfg.userSeq || '';
     blatFormCount();
 
     $('#blatUserSeq').on('input', blatFormCount);
+    // The limit shown depends on the query type, so recount when it changes.
+    $('#blatFormBox select[name=type]').on('change', blatFormCount);
     // Mirror the "Keep results" checkbox into its hidden field so an unticked box submits an
     // explicit 0 rather than nothing at all, and remember the choice in localStorage so it comes
     // back pre-set on the user's next visit (see keepResultsInit above).
     $('#blat_keepResults').on('change', function() {
         document.getElementById('blatKeepResultsVal').value = this.checked ? '1' : '0';
         blatSetKeepResultsPref(this.checked);
     });
     $('#blat_onlyLatest').on('change', function() {
         document.getElementById('blatOnlyLatestVal').value = this.checked ? '1' : '0';
         blatSetOnlyLatestPref(this.checked);
     });
     $('#blatTabPaste').on('click', function() { blatFormTab(false); });
     $('#blatTabUpload').on('click', function() { blatFormTab(true); });
     // The example sequence is a real 2.5 kb query, fetched on demand so it is not carried in every
     // page load.  The button doubles as its own status indicator while the request is in flight,