f48fc325d72424177878c4849096c77e06c2d3b0 max Mon Aug 17 07:34:42 2026 -0700 BLAT results: own track group, Delete-all button, clearer names, refs #38086 Behind hg.conf blatResultsGroup (default off, a release gate): - BLAT result custom tracks go into their own "BLAT Results" track group instead of the generic Custom Tracks group. - That group's header gets a "Delete all" button that removes every BLAT result track at once, so users are not stuck deleting them one by one. - Headerless queries are named by query size + the top hit's gene, e.g. "360bp SOD1", instead of the useless "blat YourSeq"; the date goes in the longLabel. hgBlat.c getCustomName naming + blatDateStamp/topHitLocusLabel helpers hgc.c buildBigPsl tags the results track group=blat customFactory.c checkGroup accepts the synthetic "blat" group hgTracks.c synthesize the BLAT Results group; Delete-all button; keep the per-track delete icon for the new group hgTracks.js deleteAllBlatTracks() hgCustom.c hgct_do_delete_blat action (removes all blatResult=on tracks) hgConfCatalog.py register blatResultsGroup as a release gate diff --git src/hg/hgBlat/hgBlat.c src/hg/hgBlat/hgBlat.c index 5fae564a6b7..972bedda3f9 100644 --- src/hg/hgBlat/hgBlat.c +++ src/hg/hgBlat/hgBlat.c @@ -1412,53 +1412,149 @@ { if (startsWith(customName, ct->tdb->track)) // Found a track with this name. break; } if (ct == NULL) break; safef(buffer, sizeof buffer, "%s (%d)", name, count + 1); } 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 + * 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) + { + char *raw = row[4]; + char *genes = NULL; + // 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; + else if (startsWith("in:", raw)) + genes = raw + 3; + if (genes != NULL) + { + 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); + } + } + sqlFreeResult(&sr); + hFreeConn(&locusConn); + } +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]; char description[4096]; - unsigned count = slCount(names); + +// The improved naming (query size + top-hit gene, date in the description) rides along with the +// BLAT Results track group, so it is behind the same hg.conf gate; off restores the original names. +if (!cfgOptionBooleanDefault("blatResultsGroup", FALSE)) + { if (count == 1) { safef(shortName, sizeof shortName, "blat %s", names->name); safef(description, sizeof description, "blat on %s", names->name); } else if (count == 2) { 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(); + 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); + } + 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); + 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); + } + } *pName = makeNameUnique(shortName, database, cart); *pDescription = cloneString(description); } void queryServer(char *host, char *port, char *db, struct dnaSeq *seq, char *type, char *xType, boolean complex, boolean isProt, boolean queryRC, int seqNumber, char *genomeDataDir) /* Send simple query to server and report results. (no, it doesn't do this) * queryRC is true when the query has been reverse-complemented */ { struct genomeHits *gH; AllocVar(gH); gH->host=cloneString(host);