4fd18ab7854e6620576499d1b7f70fd03d9b6f9d
max
  Thu Aug 27 09:03:04 2026 -0700
hgBlat: name a BLAT track after the gene whose exon it hits, and call protein lengths aa

#Preview2 week - bugs introduced now will need a build patch to fix
topHitLocusLabel() took whatever locusName row hRangeQuery happened to return
first, which is bin order and unrelated to the hit.  A 400 bp query with 345 bp
inside an EGFR exon came back as "400bp chr7:55019001" because the intergenic
row upstream sorted first.  It now walks all overlapping rows and prefers an
exon row over an intron row, breaking ties by how much of the hit each row
covers.

Protein searches were also labelled "154bp SOD1".  Query lengths of a protein
search are amino acids, so the track name, its description and the results page
(length and the coverage mouseover) now say "aa" for those.

refs #38086

diff --git src/hg/hgBlat/hgBlat.c src/hg/hgBlat/hgBlat.c
index e1a8620644d..5f12175762f 100644
--- src/hg/hgBlat/hgBlat.c
+++ src/hg/hgBlat/hgBlat.c
@@ -624,30 +624,32 @@
 jsIncludeDataTablesLibs();
 webIncludeResourceFile("gbModern.css");   // shared house-style components (.gbPill, .gbCard, ...)
 webIncludeResourceFile("hgBlat.css");
 jsIncludeFile("hgBlat.js", NULL);
 
 struct jsonWrite *jw = jsonWriteNew();
 jsonWriteObjectStart(jw, NULL);
 
 jsonWriteObjectStart(jw, "config");
 /* For assembly/GenArk hubs the internal names carry a "hub_NNN_" prefix; drop it so the Assembly
  * field reads cleanly (and doesn't show the prefix twice), matching the BLAT Results page title. */
 jsonWriteString(jw, "db", trackHubSkipHubName(database));
 jsonWriteString(jw, "organism", trackHubSkipHubName(organism));
 jsonWriteString(jw, "queryName", pslList->qName);
 jsonWriteNumber(jw, "querySize", pslList->qSize);
+/* Query lengths of a protein search are amino acids, so hgBlat.js labels them "aa", not "bp". */
+jsonWriteBoolean(jw, "isProt", pslIsProtein(pslList));
 jsonWriteNumber(jw, "hitCount", slCount(pslList));
 jsonWriteBoolean(jw, "multiQuery", pslListMultiQuery(pslList));
 jsonWriteBoolean(jw, "hasLocus", locusConn != NULL);
 /* Sharing a link only makes sense when a durable bigPsl custom track was made from the results
  * (autoBigPsl); otherwise there is nothing for the shared session to reopen from. */
 jsonWriteBoolean(jw, "canShare", autoBigPsl);
 /* Renaming the results custom track needs the bigPsl track + the C blatRenameCt() helper, both only
  * present with autoBigPsl.  hgBlat.js shows its own "Rename BLAT Track" button + modal in that case,
  * pre-filled with the track's current name/description (below) so it needs no page-global. */
 jsonWriteBoolean(jw, "canRename", autoBigPsl);
 if (autoBigPsl)
     {
     char *ctName = NULL, *ctDescription = NULL;
     getCustomName(database, cart, pslList, &ctName, &ctDescription);
     jsonWriteString(jw, "trackName", ctName);
@@ -1432,65 +1434,88 @@
 
 return cloneString(buffer);
 }
 
 static char *blatDateStamp()
 /* Return today's date as YYYY-MM-DD in a static buffer (same idiom as cart.c). */
 {
 static char buf[16];
 time_t now = time(NULL);
 struct tm *tm = localtime(&now);
 strftime(buf, sizeof buf, "%Y-%m-%d", tm);
 return buf;
 }
 
 static char *topHitLocusLabel(char *database, struct psl *psl)
-/* Return a short, human-recognizable label for the top BLAT hit: the first gene symbol from the
+/* Return a short, human-recognizable label for the top BLAT hit: a gene symbol from the
  * assembly's locusName table when the hit lands in a gene, otherwise a chrom:start position.
  * The locusName lookup mirrors the results page (see printBlatResultsApp).  Returns a cloneString'd
  * value the caller must free. */
 {
 struct subText *subList = NULL;
 struct sqlConnection *locusConn = blatLocusConn(database, &subList);
 char *label = NULL;
 if (locusConn != NULL)
     {
     struct sqlResult *sr = hRangeQuery(locusConn, "locusName", psl->tName, psl->tStart, psl->tEnd,
         NULL, 0);
-    char **row = sqlNextRow(sr);
-    if (row != NULL)
+    // A hit usually overlaps several locusName rows, and they come back in bin order, which has
+    // nothing to do with how much of the hit each one covers.  Pick the best row instead: an exon
+    // row always beats an intron row, however little of the exon the hit touches, because touching
+    // any coding sequence is the more informative thing to say about the query.  Within one kind,
+    // the row the hit overlaps most wins.
+    char **row;
+    char *bestGenes = NULL;
+    boolean bestIsExon = FALSE;
+    int bestOverlap = 0;
+    while ((row = sqlNextRow(sr)) != NULL)
         {
         char *raw = row[4];
         char *genes = NULL;
+        boolean isExon = FALSE;
         // Only ex:/in: (exon/intron) hits name a gene; ig: (intergenic) just lists the neighbors,
         // so for those fall through to coordinates, which are more useful.
         if (startsWith("ex:", raw))
+            {
             genes = raw + 3;
+            isExon = TRUE;
+            }
         else if (startsWith("in:", raw))
             genes = raw + 3;
-        if (genes != NULL)
+        else
+            continue;
+        int overlap = positiveRangeIntersection(psl->tStart, psl->tEnd,
+            sqlSigned(row[2]), sqlSigned(row[3]));
+        if (bestGenes == NULL || (isExon && !bestIsExon) ||
+            (isExon == bestIsExon && overlap > bestOverlap))
             {
-            char *dupe = cloneString(genes);
-            char *words[128];
-            int n = chopByChar(dupe, '|', words, ArraySize(words));
-            if (n > 0 && words[0][0] != '\0')
-                label = cloneString(words[0]);
-            freeMem(dupe);
+            freeMem(bestGenes);
+            bestGenes = cloneString(genes);
+            bestIsExon = isExon;
+            bestOverlap = overlap;
             }
         }
     sqlFreeResult(&sr);
     hFreeConn(&locusConn);
+    if (bestGenes != NULL)
+        {
+        char *words[128];
+        int n = chopByChar(bestGenes, '|', words, ArraySize(words));
+        if (n > 0 && words[0][0] != '\0')
+            label = cloneString(words[0]);
+        freeMem(bestGenes);
+        }
     }
 if (label == NULL)
     {
     char buf[128];
     safef(buf, sizeof buf, "%s:%d", psl->tName, psl->tStart + 1);
     label = cloneString(buf);
     }
 return label;
 }
 
 static void getCustomName(char *database, struct cart *cart, struct psl *psl, char **pName, char **pDescription)
 // Find a track name that isn't currently a custom track. Also fill in description.
 {
 struct slName *names = namesInPsl(psl);
 char shortName[4096];
@@ -1511,45 +1536,49 @@
         safef(shortName, sizeof shortName, "blat %s+%d", names->name, count - 1);
         safef(description, sizeof description, "blat on %d queries (%s, %s)", count, names->name, names->next->name);
         }
     else
         {
         safef(shortName, sizeof shortName, "blat %s+%d", names->name, count - 1);
         safef(description, sizeof description, "blat on %d queries (%s, %s, ...)", count, names->name, names->next->name);
         }
     }
 else
     {
     // The short label (track name) drops the "BLAT" word - these tracks already live in the "BLAT
     // Results" group, so the prefix would just be noise.  The long label keeps "BLAT" for context
     // wherever it shows without the group heading (e.g. the custom-track manager, mouseovers).
     char *date = blatDateStamp();
+    // A protein query is measured in amino acids, not bases, so do not call its length "bp".
+    char *unit = pslIsProtein(psl) ? "aa" : "bp";
     if (count == 1)
         {
         if (differentString(names->name, "YourSeq"))
             {
             // Query carried a FASTA header, so name the track after the sequence.
             safef(shortName, sizeof shortName, "%s", names->name);
-            safef(description, sizeof description, "BLAT %s, %dbp, %s", names->name, psl->qSize, date);
+            safef(description, sizeof description, "BLAT %s, %d%s, %s", names->name, psl->qSize,
+                unit, date);
             }
         else
             {
             // Headerless query: "YourSeq" tells the user nothing, so name it by query size and the
             // top hit's gene (or position), which is what they actually recognize later.
             char *locus = topHitLocusLabel(database, psl);
-            safef(shortName, sizeof shortName, "%dbp %s", psl->qSize, locus);
-            safef(description, sizeof description, "BLAT %dbp %s, %s", psl->qSize, locus, date);
+            safef(shortName, sizeof shortName, "%d%s %s", psl->qSize, unit, locus);
+            safef(description, sizeof description, "BLAT %d%s %s, %s", psl->qSize, unit, locus,
+                date);
             freeMem(locus);
             }
         }
     else if (count == 2)
         {
         safef(shortName, sizeof shortName, "%s+%d", names->name, count - 1);
         safef(description, sizeof description, "BLAT %d queries (%s, %s), %s", count, names->name, names->next->name, date);
         }
     else
         {
         safef(shortName, sizeof shortName, "%s+%d", names->name, count - 1);
         safef(description, sizeof description, "BLAT %d queries (%s, %s, ...), %s", count, names->name, names->next->name, date);
         }
     }