98faa189349f1bb9b033f314ffe327541bacf290
chmalee
  Tue Mar 31 15:17:30 2026 -0700
Add a new hgFindSpec setting, searchItemLabel, that allows using hgFindSpec defined labels (with variable substitution) as the label for each line of a search result. This only works for bigBed tracks. refs #37299

diff --git src/hg/lib/bigBedFind.c src/hg/lib/bigBedFind.c
index 318c93a76e7..2329a842b6e 100644
--- src/hg/lib/bigBedFind.c
+++ src/hg/lib/bigBedFind.c
@@ -11,102 +11,121 @@
 #include "bigBedLabel.h"
 #include "bigBedFind.h"
 #include "genbank.h"
 
 static struct hgPos *bigBedIntervalListToHgPositions(struct cart *cart, struct trackDb *tdb,
                         struct bbiFile *bbi, char *term, struct bigBedInterval *intervalList,
                         char *description, struct hgFindSpec *hfs, char *db)
 /* Given an open bigBed file, and an interval list, return a pointer to a list of hgPos structures. */
 {
 struct hgPos *posList = NULL;
 char chromName[bbi->chromBpt->keySize+1];
 int lastChromId = -1;
 struct bigBedInterval *interval;
 struct slInt *labelColumns = NULL;
 
+// Generic searchItemLabel support: allows ${fieldName} patterns in hgFindSpec
+char *searchItemLabel = NULL;
+char **fieldNames = NULL;
+if (hfs)
+    searchItemLabel = hgFindSpecSetting(hfs, "searchItemLabel");
+if (searchItemLabel)
+    {
+    AllocArray(fieldNames, bbi->fieldCount);
+    struct slName *field = NULL, *fields = bbFieldNames(bbi);
+    int i = 0;
+    for (field = fields; field != NULL && i < bbi->fieldCount; field = field->next)
+        fieldNames[i++] = field->name;
+    }
+
+// MANE/HGNC description support
 struct asObject *as = NULL;
 int ncbiIdIx = -1, geneNameIx = -1;
 struct sqlConnection *conn = NULL;
 if (hfs && (sameString(hfs->searchName, "mane") || sameString(hfs->searchName, "hgnc")))
     {
-    // TODO: right now we are only doing this for MANE and HGNC, but if we are gonna add
-    // special descriptions to more tracks in the future then we should have some generic
-    // way of attaching a description to an hfs, whether that hfs is a mysql search or a
-    // bigBed search
     as = bigBedAsOrDefault(bbi);
     if (sameString(hfs->searchName, "mane"))
         {
         conn = hAllocConn(db);
         ncbiIdIx = asColumnFindIx(as->columnList, "ncbiId");
         }
     else if (sameString(hfs->searchName, "hgnc"))
         geneNameIx = asColumnFindIx(as->columnList, "geneName");
     }
 
 bigBedLabelCalculateFields(cart, tdb, bbi,  &labelColumns );
+char startBuf[256], endBuf[256], *row[bbi->fieldCount];
 for (interval = intervalList; interval != NULL; interval = interval->next)
     {
     struct hgPos *hgPos;
     AllocVar(hgPos);
     slAddHead(&posList, hgPos);
 
     bbiCachedChromLookup(bbi, interval->chromId, lastChromId, chromName, sizeof(chromName));
     lastChromId = interval->chromId;
 
     hgPos->chrom = cloneString(chromName);
     hgPos->chromStart = interval->start;
     hgPos->chromEnd = interval->end;
-    hgPos->name = bigBedMakeLabel(tdb, labelColumns, interval, chromName);
     hgPos->browserName = cloneString(term);
     hgPos->description = cloneString(description);
+
+    if (searchItemLabel)
+        {
+        bigBedIntervalToRow(interval, chromName, startBuf, endBuf, row, bbi->fieldCount);
+        hgPos->name = replaceFieldInPattern(searchItemLabel, bbi->fieldCount, fieldNames, row);
+        }
+    else
+        {
+        hgPos->name = bigBedMakeLabel(tdb, labelColumns, interval, chromName);
+        if (hfs && (sameString(hfs->searchName, "mane") || sameString(hfs->searchName, "hgnc")))
+            bigBedIntervalToRow(interval, chromName, startBuf, endBuf, row, bbi->fieldCount);
+        }
+
     if (hfs)
         {
         char *paddingStr = hgFindSpecSetting(hfs, "padding");
         int padding = isEmpty(paddingStr) ? 0 : atoi(paddingStr);
         if (padding > 0)
             {
             // highlight the item bases only, to distinguish from padding
             hgPos->highlight = addHighlight(cartString(cart, "db"),
                                             hgPos->chrom, hgPos->chromStart, hgPos->chromEnd);
             hgPos->chromStart -= padding;
             hgPos->chromEnd   += padding;
             if (hgPos->chromStart < 0)
                 hgPos->chromStart = 0;
             }
-        // special code here for per hfs bigBed searches
-        if (sameString(hfs->searchName, "mane") || sameString(hfs->searchName, "hgnc"))
-            {
-            char startBuf[256], endBuf[256], *row[bbi->fieldCount];
-            bigBedIntervalToRow(interval, chromName, startBuf, endBuf, row, bbi->fieldCount);
+        // MANE/HGNC description logic
         if (sameString(hfs->searchName, "mane"))
             {
             // here the description comes from hgFixed.refLink.product, linked via mane.bb.ncbiId
             if (ncbiIdIx > 0)
                 {
                 struct dyString *query = sqlDyStringCreate("select product from %s where mrnaAcc=substring_index('%s', '.', 1)", refLinkTable, row[ncbiIdIx]);
                 hgPos->description = sqlQuickString(conn, dyStringCannibalize(&query));
                 }
             }
-            else
+        else if (sameString(hfs->searchName, "hgnc"))
             {
             // the description is the geneName field of the bigBed
             if (geneNameIx > 0)
                 hgPos->description = cloneString(row[geneNameIx]);
             }
         }
     }
-    }
 
 if (conn)
     hFreeConn(&conn);
 
 return posList;
 }
 
 static struct hgPos *getPosFromBigBed(struct cart *cart, struct trackDb *tdb, struct bbiFile *bbi,
                         char *indexField, char *term, char *description, struct hgFindSpec *hfs, char *db)
 /* Given a bigBed file with a search index, check for term. */
 {
 struct errCatch *errCatch = errCatchNew();
 struct hgPos *posList = NULL;
 if (errCatchStart(errCatch))
     {