2521d696f5073ce8cee3f59f423f161fdb77d550
max
  Fri Sep 25 02:48:37 2026 -0700
VCF tracks: the bigBed trackDb filters (filter.*, filterByRange, filterLimits, filterValues, filterType, filterText, filterLabel) now work on INFO fields, plus ID and QUAL. The field list and types come from the VCF header; the bigBed filter code is reused, bigBed behavior unchanged, refs #37617

diff --git src/hg/lib/hui.c src/hg/lib/hui.c
index 54b082a5564..8cacb88d5c3 100644
--- src/hg/lib/hui.c
+++ src/hg/lib/hui.c
@@ -4031,30 +4031,41 @@
 safef(labelSetting, sizeof labelSetting, "%s.%s", FILTER_LABEL_NAME_LOW, field);
 char *trackDbLabel = cartOrTdbString(cart, tdb, labelSetting, NULL);
 if (trackDbLabel == NULL)
     {
     safef(labelSetting, sizeof labelSetting, "%s.%s", field, FILTER_LABEL_NAME_CAP);
     trackDbLabel = cartOrTdbString(cart, tdb, labelSetting, NULL);
     }
 if (trackDbLabel == NULL)
     {
     safef(labelSetting, sizeof labelSetting, "%s%s", field, FILTER_LABEL_NAME_CAP);
     trackDbLabel = cartOrTdbString(cart, tdb, labelSetting, NULL);
     }
 return trackDbLabel;
 }
 
+struct asObject *asForTrackDbFilters(struct cart *cart, struct sqlConnection *conn, struct trackDb *tdb)
+/* Return the autoSql object whose column names the trackDb filter settings (filter.<field>,
+ * filterText.<field>, filterValues.<field>, ...) refer to, or NULL.  For VCF tracks these are the
+ * INFO fields declared in the VCF header (plus ID and QUAL), for all other tracks the track's
+ * autoSql. */
+{
+if (tdbIsVcf(tdb))
+    return vcfInfoAsObj(cart, tdb);
+return asForTdb(conn, tdb);
+}
+
 static filterBy_t *buildFilterBy(struct trackDb *tdb, struct cart *cart, struct asObject *as, struct trackDbFilter *tdbFilter, char *name)
 /* Build a filterBy_t structure from a <column>FilterValues statement. */
 {
 boolean isHighlight = startsWith("highlightValues.", tdbFilter->name);
 char *field = tdbFilter->fieldName;
 if (isEmpty(tdbFilter->setting))
     errAbort("track %s: FilterValues setting of field '%s' must have a value.", tdb->track, tdbFilter->fieldName);
 
 char *value = cartUsualStringClosestToHome(cart, tdb, FALSE, tdbFilter->name, tdbFilter->setting);
 
 filterBy_t *filterBy;
 AllocVar(filterBy);
 filterBy->column = cloneString(field);
 filterBy->title = cloneString(field); ///  title should come from AS file, or trackDb variable
 struct asColumn *asCol = (as != NULL) ? asColumnFind(as, field) : NULL;
@@ -4100,31 +4111,31 @@
 
 struct dyString *dy = dyStringNew(128);
 dyStringPrintf(dy, "%s.%s.%s", name, isHighlight ? "highlightBy": "filterBy", filterBy->column);
 filterBy->htmlName = dy->string;
 
 return filterBy;
 }
 
 filterBy_t *filterByValues(struct trackDb *tdb, struct cart *cart, struct trackDbFilter *trackDbFilters, char *name)
 /* Build a filterBy_t list from tdb variables of the form *FilterValues */
 {
 // Not every tdb has an autoSql: superTracks and tracks pointing at a
 // bigData file that isn't reachable at UI time both return NULL here.
 // That's fine for filterValues.* settings as long as a filterLabel.*
 // override is provided; buildFilterBy() already tolerates a NULL `as`.
-struct asObject *as = asForTdb(NULL, tdb);
+struct asObject *as = asForTrackDbFilters(cart, NULL, tdb);
 filterBy_t *filterByList = NULL, *filter;
 struct trackDbFilter *fieldFilter;
 while ((fieldFilter = slPopHead(&trackDbFilters)) != NULL)
     {
     // slAddTail (not slAddHead) keeps the priority-sorted order from tdbGetTrackFilters
     // since filterBySetCfgUi displays the list head-to-tail.
     if ((filter = buildFilterBy(tdb, cart, as, fieldFilter, name)) != NULL)
         slAddTail(&filterByList, filter);
     }
 return filterByList;
 }
 
 filterBy_t *filterBySetGetGuts(struct trackDb *tdb, struct cart *cart, char *name, char *subName, char *settingName)
 // Gets one or more "filterBy" settings (ClosestToHome).  returns NULL if not found
 {
@@ -7038,31 +7049,31 @@
 // Shows all *Filter style filters.  Note that these are in random order and have no graceful title
 {
 int count = 0;
 struct trackDbFilter *trackDbFilters = NULL;
 if (isHighlight)
     trackDbFilters = tdbGetTrackNumHighlights(tdb);
 else
     trackDbFilters = tdbGetTrackNumFilters(tdb);
 if (trackDbFilters)
     {
     puts("<BR>");
     struct trackDbFilter *filter = NULL;
     struct sqlConnection *conn = NULL;
     if (!isHubTrack(db) && !isGenArk(db))
         conn = hAllocConnTrack(db, tdb);
-    struct asObject *as = asForTdb(conn, tdb);
+    struct asObject *as = asForTrackDbFilters(cart, conn, tdb);
     hFreeConn(&conn);
 
     while ((filter = slPopHead(&trackDbFilters)) != NULL)
         {
         char *field = filter->fieldName;
         char *scoreName = cloneString(filter->name);
         char *trackDbLabel = getLabelSetting(cart, tdb, field);
 
         if (as != NULL)
             {
             struct asColumn *asCol = asColumnFind(as, field);
             if (asCol != NULL)
                 { // Found label so replace field; strip "|..." suffix used for detail page
                 field = asCol->comment;
                 char *pipe = strchr(field, '|');
@@ -7182,31 +7193,31 @@
 /* Show all the text filters for this track. */
 {
 int count = 0;
 struct trackDbFilter *trackDbFilters = NULL;
 if (isHighlight)
     trackDbFilters = tdbGetTrackTextHighlights(tdb);
 else
     trackDbFilters = tdbGetTrackTextFilters(tdb);
 if (trackDbFilters)
     {
     puts("<BR>");
     struct trackDbFilter *filter = NULL;
     struct sqlConnection *conn = NULL;
     if (!isHubTrack(db) && !isGenArk(db))
         conn = hAllocConnTrack(db, tdb);
-    struct asObject *as = asForTdb(conn, tdb);
+    struct asObject *as = asForTrackDbFilters(cart, conn, tdb);
     hFreeConn(&conn);
     while ((filter = slPopHead(&trackDbFilters)) != NULL)
         {
         char *trackDbLabel = getLabelSetting(cart, tdb, filter->fieldName);
         char *value = cartUsualStringClosestToHome(cart, tdb, FALSE, filter->name, filter->setting);
         if (as != NULL)
             {
             struct asColumn *asCol = asColumnFind(as, filter->fieldName);
             if (asCol != NULL)
                 {
                 if (trackDbLabel == NULL)
                     trackDbLabel = asCol->comment;
                 }
             else if (defaultFieldLocation(filter->fieldName) < 0)
                 errAbort("Building filter on field %s which is not in AS file.", filter->fieldName);
@@ -7232,69 +7243,103 @@
         printf("</SELECT>");
         printf("&nbsp;&nbsp;<button class='buttonClear-%s'>Reset</button>\n", tdb->track);
         printf("</P>");
         }
         // using jquery id= syntax to make sure that selector works even if trackname has a dot in it
         jsInlineF("$('[class=\"buttonClear-%s\"]').click( function(ev) { \n"
                     "$(ev.target).prevAll('input').val('*').trigger('change');\n"
                     "$(ev.target).prevAll('select').val('%s');\n"
                     "ev.preventDefault();\n"
                   "});", tdb->track, FILTERTEXT_WILDCARD);
     }
 
 return count;
 }
 
+static boolean trackDbFiltersShowAll(char *db, struct cart *cart, struct trackDb *tdb,
+                                     boolean *pIsBoxOpened, boolean boxed, boolean parentLevel,
+                                     char *name, char *title)
+// Show the numeric (filter.*), text (filterText.*) and multi-select (filterValues.*, filterBy)
+// filters of a track, but no highlights.  Returns TRUE if any filter was shown.
+{
+boolean gotFilter = FALSE;
+if (numericFiltersShowAll(db, cart, tdb, pIsBoxOpened, boxed, parentLevel, name, title, FALSE) > 0)
+    gotFilter = TRUE;
+
+if (textFiltersShowAll(db, cart, tdb, FALSE))
+    gotFilter = TRUE;
+
+// Add any multi-selects next
+filterBy_t *filterBySet = filterBySetGet(tdb,cart,name);
+if (filterBySet != NULL)
+    {
+    if (!tdbIsComposite(tdb) && cartOptionalString(cart, "ajax") == NULL)
+        jsIncludeFile("hui.js",NULL);
+
+    if (!*pIsBoxOpened)   // Note filterBy boxes are not double "boxed",
+        printf("<BR>"); // if there are no other filters
+    filterBySetCfgUi(cart,tdb,filterBySet,TRUE, name);
+    filterBySetFree(&filterBySet);
+    gotFilter = TRUE;
+    }
+return gotFilter;
+}
+
+boolean trackDbFiltersCfgUi(char *db, struct cart *cart, struct trackDb *tdb, char *name,
+                            boolean boxed)
+// Put up only the generic trackDb filter controls of a track (filter.*, filterText.*,
+// filterValues.* and their older *Filter forms), without the score filter and highlights.
+// For track types with their own configuration UI, e.g. VCF, where the fields are INFO keys.
+// Returns TRUE if any filter control was shown.
+{
+if (cartOptionalString(cart, "ajax") == NULL)
+    {
+    webIncludeResourceFile("ui.dropdownchecklist.css");
+    jsIncludeFile("ui.dropdownchecklist.js",NULL);
+    jsIncludeFile("ddcl.js",NULL);
+    }
+boolean parentLevel = isNameAtParentLevel(tdb,name);
+boolean isBoxOpened = FALSE;
+boolean gotFilter = trackDbFiltersShowAll(db, cart, tdb, &isBoxOpened, boxed, parentLevel,
+                                          name, NULL);
+if (isBoxOpened)
+    cfgEndBox(boxed);
+return gotFilter;
+}
+
 void scoreCfgUi(char *db, struct cart *cart, struct trackDb *tdb, char *name, char *title,
                 int maxScore, boolean boxed)
 // Put up UI for filtering bed track based on a score
 {
 char option[256];
 if (cartOptionalString(cart, "ajax") == NULL)
     {
     webIncludeResourceFile("ui.dropdownchecklist.css");
     jsIncludeFile("ui.dropdownchecklist.js",NULL);
     jsIncludeFile("ddcl.js",NULL);
     }
 
 boolean parentLevel = isNameAtParentLevel(tdb,name);
 if (parentLevel)
     if (trackDbSettingOn(tdb->parent, "noParentConfig"))
         return;
 boolean skipScoreFilter = FALSE;
 
-// Numeric filters are first
+// Numeric filters are first, then text filters, then the multi-selects
 boolean isBoxOpened = FALSE;
-if (numericFiltersShowAll(db, cart, tdb, &isBoxOpened, boxed, parentLevel, name, title, FALSE) > 0)
-    skipScoreFilter = TRUE;
-
-if (textFiltersShowAll(db, cart, tdb, FALSE))
-    skipScoreFilter = TRUE;
-
-// Add any multi-selects next
-filterBy_t *filterBySet = filterBySetGet(tdb,cart,name);
-if (filterBySet != NULL)
-    {
-    if (!tdbIsComposite(tdb) && cartOptionalString(cart, "ajax") == NULL)
-        jsIncludeFile("hui.js",NULL);
-
-    if (!isBoxOpened)   // Note filterBy boxes are not double "boxed",
-        printf("<BR>"); // if there are no other filters
-    filterBySetCfgUi(cart,tdb,filterBySet,TRUE, name);
-    filterBySetFree(&filterBySet);
+if (trackDbFiltersShowAll(db, cart, tdb, &isBoxOpened, boxed, parentLevel, name, title))
     skipScoreFilter = TRUE;
-    }
 
 // add any highlights:
 // Numeric highlights are first
 if (numericFiltersShowAll(db, cart, tdb, &isBoxOpened, boxed, parentLevel, name, title, TRUE) > 0)
     {
     didHighlightSelector = TRUE;
     skipScoreFilter = TRUE;
     }
 
 if (textFiltersShowAll(db, cart, tdb, TRUE))
     {
     didHighlightSelector = TRUE;
     skipScoreFilter = TRUE;
     }