1db5c89d97fdd3c3580c4ba9b18eba6b5029c97d max Thu Sep 17 05:09:56 2026 -0700 hgSearch: highlight MANE transcript(s) in RefSeq protein-position search results, refs #38285 #Preview2 week - bugs introduced now will need a build patch to fix A gene-symbol + codon-range search (e.g. "BRCA1 100-200") maps to every RefSeq isoform prediction sharing that genomic footprint, which can be dozens of near-identical NP_ accessions with no indication of which one is the clinically-relevant MANE transcript. Cross-reference the mane bigGenePred track by genomic overlap + protein accession and call out the MANE Select/Plus Clinical transcript(s) in their own section above the rest, gated behind showManeInSearch (default off) in hg.conf. diff --git src/hg/lib/bigBedFind.c src/hg/lib/bigBedFind.c index 70f19589be0..3e195b0d85f 100644 --- src/hg/lib/bigBedFind.c +++ src/hg/lib/bigBedFind.c @@ -1,481 +1,582 @@ #include "common.h" #include "bPlusTree.h" #include "bbiFile.h" #include "bigBed.h" #include "hgFind.h" #include "trix.h" #include "trackHub.h" #include "hubConnect.h" #include "hdb.h" #include "errCatch.h" #include "bigBedLabel.h" #include "bigBedFind.h" #include "genbank.h" #include "quickLift.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; // If this track is being quickLifted, the bigBed returns hits in the source // assembly's coordinates; we need to remap them to the destination assembly. char *quickLiftDb = trackDbSetting(tdb, "quickLiftDb"); boolean quickLifted = (trackDbSetting(tdb, "quickLiftUrl") != NULL) && isNotEmpty(quickLiftDb); char *destDb = trackHubSkipHubName(db); // 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"))) { 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) { bbiCachedChromLookup(bbi, interval->chromId, lastChromId, chromName, sizeof(chromName)); lastChromId = interval->chromId; char *posChrom = chromName; int posStart = interval->start; int posEnd = interval->end; if (quickLifted) { // Remap source-assembly coords to destination-assembly coords; skip if unmappable. if (!quickLiftLiftPos(quickLiftDb, destDb, chromName, interval->start, interval->end, &posChrom, &posStart, &posEnd)) continue; } struct hgPos *hgPos; AllocVar(hgPos); slAddHead(&posList, hgPos); hgPos->chrom = cloneString(posChrom); hgPos->chromStart = posStart; hgPos->chromEnd = posEnd; hgPos->description = cloneString(description); int rowFieldCount = bigBedIntervalToRow(interval, chromName, startBuf, endBuf, row, bbi->fieldCount); if (searchItemLabel) { hgPos->name = replaceFieldInPattern(searchItemLabel, bbi->fieldCount, fieldNames, row); } else { hgPos->name = bigBedMakeLabel(tdb, labelColumns, interval, chromName); } // browserName needs to correspond to tg->mapItemName() if (rowFieldCount > 3) hgPos->browserName = cloneString(row[3]); else hgPos->browserName = cloneString(term); 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; } // 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 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)) { int fieldIx; struct bptFile *bpt = bigBedOpenExtraIndex(bbi, indexField, &fieldIx); struct lm *lm = lmInit(0); struct bigBedInterval *intervalList; intervalList = bigBedNameQuery(bbi, bpt, fieldIx, term, lm); posList = bigBedIntervalListToHgPositions(cart, tdb, bbi, term, intervalList, description, hfs, db); bptFileDetach(&bpt); } errCatchEnd(errCatch); if (errCatch->gotError) { // we fail silently if there is a problem e.g. bad index name return NULL; } return posList; } static struct hgPos *doTrixSearch(struct cart *cart, struct trackDb *tdb, char *trixFile, struct slName *indices, struct bbiFile *bbi, char *term, char *description, struct hgFindSpec *hfs, char *db) /* search a trix file in the "searchTrix" field of a bigBed trackDb */ { struct trix *trix = trixOpen(trixFile); int trixWordCount = 0; char *tmp = cloneString(term); char *val = nextWord(&tmp); char *trixWords[128]; while (val != NULL) { trixWords[trixWordCount] = strLower(val); trixWordCount++; if (trixWordCount == sizeof(trixWords)/sizeof(char*)) errAbort("exhausted space for trixWords"); val = nextWord(&tmp); } if (trixWordCount == 0) return NULL; struct trixSearchResult *tsList = trixSearch(trix, trixWordCount, trixWords, tsmExpand); char *context = NULL; if (hfs) context = hgFindSpecSetting(hfs, "searchTrixContext"); boolean doSnippets = FALSE; if (context && sameString(context, "on")) { doSnippets = TRUE; initSnippetIndex(trix); } struct hgPos *posList = NULL; for ( ; tsList != NULL; tsList = tsList->next) { struct slName *oneIndex = indices; if (doSnippets) addSnippetForResult(tsList, trix); for (; oneIndex; oneIndex = oneIndex->next) { struct hgPos *posList2 = getPosFromBigBed(cart, tdb, bbi, oneIndex->name, tsList->itemId, tsList->snippet, hfs, db); posList = slCat(posList, posList2); } } return posList; } int posListCompare(const void *va, const void *vb) /* Compare to sort based on name and then position. */ { const struct hgPos *a = *((struct hgPos **)va); const struct hgPos *b = *((struct hgPos **)vb); int diff = strcmp(a->name, b->name); if (diff == 0) { diff = strcmp(a->chrom, b->chrom); if (diff == 0) { diff = a->chromStart - b->chromStart; if (diff == 0) diff = a->chromEnd - b->chromEnd; } } return diff; } boolean findBigBedPosInTdb(struct cart *cart, char *db, struct trackDb *tdb, char *term, struct hgPositions *hgp, struct hgFindSpec *hfs, boolean measureTiming) /* Find a position in a single trackDb entry */ { if (startsWith("bigWig", tdb->type) || !startsWith("big", tdb->type)) return FALSE; long startTime = clock1000(); boolean found = FALSE; char *description = NULL; if (hfs) { char buf[2048]; if (isNotEmpty(hfs->searchDescription)) truncatef(buf, sizeof(buf), "%s", hfs->searchDescription); else safef(buf, sizeof(buf), "%s", hfs->searchTable); description = cloneString(buf); } // Which field(s) to search? Look for searchIndex in search spec, then in trackDb for // backwards compat. char *indexField = NULL; if (hfs) indexField = hgFindSpecSetting(hfs, "searchIndex"); if (!indexField) indexField = trackDbSetting(tdb, "searchIndex"); if (!indexField && !hfs) return FALSE; // If !indexField but we do have a non-NULL hfs, then open file to see if it has a name index. char *fileName = trackDbSetting(tdb, "bigDataUrl"); if (!fileName && !trackHubDatabase(db)) { struct sqlConnection *conn = hAllocConnTrack(db, tdb); fileName = bbiNameFromSettingOrTable(tdb, conn, tdb->table); hFreeConn(&conn); } if (!fileName) return FALSE; // we fail silently if bigBed can't be opened. struct bbiFile *bbi = NULL; struct errCatch *errCatch = errCatchNew(); if (errCatchStart(errCatch)) { bbi = bigBedFileOpen(fileName); } errCatchEnd(errCatch); if (errCatch->gotError) return FALSE; // Now (since hfs is non-NULL) check the file to see if it has a name index if we // don't already have indexField. if (!indexField) { struct slName *indexFields = bigBedListExtraIndexes(bbi); if (slNameInList(indexFields, "name")) indexField = "name"; slNameFreeList(&indexFields); } if (!indexField) { bigBedFileClose(&bbi); return FALSE; } struct slName *indexList = slNameListFromString(indexField, ','); struct hgPos *posList1 = NULL, *posList2 = NULL; char *trixFile = trackDbSetting(tdb, "searchTrix"); // if there is a trix file, use it to search for the term if (trixFile != NULL) { struct errCatch *errCatch = errCatchNew(); if (errCatchStart(errCatch)) { posList1 = doTrixSearch(cart, tdb, hReplaceGbdb(trixFile), indexList, bbi, term, NULL, hfs, db); } errCatchEnd(errCatch); if (errCatch->gotError) warn("trix search failure for %s: %s", tdb->table, dyStringContents(errCatch->message)); errCatchFree(&errCatch); } // now search for the raw id's struct slName *oneIndex=indexList; for (; oneIndex; oneIndex = oneIndex->next) { posList2 = getPosFromBigBed(cart, tdb, bbi, oneIndex->name, term, NULL, hfs, db); posList1 = slCat(posList1, posList2); } // the trix search and the id search may have found the same item so uniqify: slUniqify(&posList1, posListCompare, hgPosFree); if (posList1 != NULL) { struct hgPosTable *table; found = TRUE; AllocVar(table); slAddHead(&hgp->tableList, table); table->description = cloneString(description ? description : tdb->longLabel); table->name = cloneString(tdb->table); table->searchTime = -1; if (measureTiming) table->searchTime = clock1000() - startTime; table->posList = posList1; } bigBedFileClose(&bbi); freeMem(description); return found; } boolean findBigBedPosInTdbList(struct cart *cart, char *db, struct trackDb *tdbList, char *term, struct hgPositions *hgp, struct hgFindSpec *hfs, boolean measureTiming) /* Given a list of trackDb entries, check each of them for a searchIndex */ { struct trackDb *tdb; boolean found = FALSE; for (tdb = tdbList; tdb; tdb = tdb->next) { if (tdb->subtracks) { found = findBigBedPosInTdbList(cart, db, tdb->subtracks, term, hgp, hfs, measureTiming) || found; continue; } found = findBigBedPosInTdb(cart, db, tdb, term, hgp, hfs, measureTiming) || found; } return found; } boolean isTdbSearchable(struct trackDb *tdb) /* Check if a single tdb is searchable */ { if (tdb->subtracks) { boolean searchable = FALSE; struct trackDb *sub; for (sub = tdb->subtracks; sub != NULL; sub = sub->next) searchable |= isTdbSearchable(sub); return searchable; } if (startsWith("bigWig", tdb->type) || !startsWith("big", tdb->type)) return FALSE; char *indexField = NULL; indexField = trackDbSetting(tdb, "searchIndex"); if (indexField) return TRUE; // If !indexField but we do have an index on the bigBed use that char *fileName = trackDbSetting(tdb, "bigDataUrl"); if (!fileName) return FALSE; // we fail silently if bigBed can't be opened. struct bbiFile *bbi = NULL; struct errCatch *errCatch = errCatchNew(); if (errCatchStart(errCatch)) { bbi = bigBedFileOpen(fileName); } errCatchEnd(errCatch); if (errCatch->gotError) return FALSE; if (!indexField) { struct slName *indexFields = bigBedListExtraIndexes(bbi); if (slNameInList(indexFields, "name")) indexField = "name"; slNameFreeList(&indexFields); } if (!indexField) { bigBedFileClose(&bbi); return FALSE; } return TRUE; } struct trackDb *getSearchableBigBeds(struct trackDb *tdbList) /* Given a list of tracks from a hub, return those that are searchable */ { struct trackDb *tdb, *next, *ret = NULL; for (tdb = tdbList ; tdb; tdb = next) { next = tdb->next; if (tdb->subtracks) { struct trackDb *subtrackList = getSearchableBigBeds(tdb->subtracks); if (subtrackList) ret = slCat(subtrackList, ret); continue; } if (startsWith("bigWig", tdb->type) || !startsWith("big", tdb->type)) continue; char *indexField = NULL; indexField = trackDbSetting(tdb, "searchIndex"); if (!indexField) continue; // If !indexField but we do have an index on the bigBed use that char *fileName = trackDbSetting(tdb, "bigDataUrl"); if (!fileName) continue; // we fail silently if bigBed can't be opened. struct bbiFile *bbi = NULL; struct errCatch *errCatch = errCatchNew(); if (errCatchStart(errCatch)) { bbi = bigBedFileOpen(fileName); } errCatchEnd(errCatch); if (errCatch->gotError) continue; if (!indexField) { struct slName *indexFields = bigBedListExtraIndexes(bbi); if (slNameInList(indexFields, "name")) indexField = "name"; slNameFreeList(&indexFields); } if (!indexField) { bigBedFileClose(&bbi); continue; } // finally we have verified the track is searchable, add it to our list we're returning // careful not to mess up the old list slAddHead(&ret, CloneVar(tdb)); } return ret; } + +struct maneLookup +/* An open handle on the "mane" bigGenePred track, for repeated region lookups. */ + { + struct bbiFile *bbi; + int protAccIx; + int maneStatIx; + }; + +struct maneLookup *maneLookupOpen(char *db, struct trackDb **tdbList) +/* Open the "mane" bigGenePred track for db, if it exists, for repeated calls to + * maneStatusForRegion. Returns NULL if this assembly has no mane track (e.g. non-human, + * or hg19) -- callers should treat that as "can't tell", not an error. tdbList is passed + * through to tdbForTrack, so pass a pointer already shared with other tdbForTrack calls in + * the same request to avoid a second trackDb load. */ +{ +struct trackDb *tdb = tdbForTrack(db, "mane", tdbList); +if (tdb == NULL) + return NULL; +char *fileName = trackDbSetting(tdb, "bigDataUrl"); +if (isEmpty(fileName)) + return NULL; + +struct maneLookup *ml = NULL; +struct errCatch *errCatch = errCatchNew(); +if (errCatchStart(errCatch)) + { + struct bbiFile *bbi = bigBedFileOpen(hReplaceGbdb(fileName)); + struct asObject *as = bigBedAsOrDefault(bbi); + AllocVar(ml); + ml->bbi = bbi; + ml->protAccIx = asColumnFindIx(as->columnList, "ncbiProtAcc"); + ml->maneStatIx = asColumnFindIx(as->columnList, "maneStat"); + } +errCatchEnd(errCatch); +if (errCatch->gotError) + ml = NULL; +errCatchFree(&errCatch); +return ml; +} + +static boolean protAccMatches(char *rowProtAcc, struct slName *protAccList) +/* Return TRUE if rowProtAcc matches one of protAccList, ignoring version (".N") suffixes. */ +{ +char *rowDot = strchr(rowProtAcc, '.'); +int rowLen = rowDot ? (rowDot - rowProtAcc) : (int)strlen(rowProtAcc); +struct slName *pa; +for (pa = protAccList; pa != NULL; pa = pa->next) + { + char *paDot = strchr(pa->name, '.'); + int paLen = paDot ? (paDot - pa->name) : (int)strlen(pa->name); + if (rowLen > 0 && rowLen == paLen && memcmp(rowProtAcc, pa->name, rowLen) == 0) + return TRUE; + } +return FALSE; +} + +char *maneStatusForRegion(struct maneLookup *ml, char *chrom, int start, int end, + struct slName *protAccList, char **retProtAcc) +/* Look for a MANE transcript overlapping chrom:start-end whose NCBI protein accession + * (ignoring version suffix) matches one of protAccList. Returns a cloned "MANE Select" / + * "MANE Plus Clinical" string, or NULL if ml is NULL or there is no match. On a match, + * *retProtAcc is set to a cloned copy of the matching (versioned) NCBI protein accession, + * so callers can identify the single MANE transcript out of a group of merged accessions. */ +{ +if (ml == NULL || ml->protAccIx < 0 || ml->maneStatIx < 0) + return NULL; +char *result = NULL; +struct lm *lm = lmInit(0); +struct errCatch *errCatch = errCatchNew(); +if (errCatchStart(errCatch)) + { + struct bigBedInterval *intervalList = bigBedIntervalQuery(ml->bbi, chrom, start, end, 0, lm); + struct bigBedInterval *interval; + char startBuf[16], endBuf[16], *row[ml->bbi->fieldCount]; + for (interval = intervalList; interval != NULL && result == NULL; interval = interval->next) + { + bigBedIntervalToRow(interval, chrom, startBuf, endBuf, row, ml->bbi->fieldCount); + if (protAccMatches(row[ml->protAccIx], protAccList)) + { + result = cloneString(row[ml->maneStatIx]); + if (retProtAcc != NULL) + *retProtAcc = cloneString(row[ml->protAccIx]); + } + } + } +errCatchEnd(errCatch); +errCatchFree(&errCatch); +lmCleanup(&lm); +return result; +} + +void maneLookupClose(struct maneLookup **pMl) +/* Close a maneLookup opened by maneLookupOpen. */ +{ +if (pMl != NULL && *pMl != NULL) + { + bigBedFileClose(&(*pMl)->bbi); + freez(pMl); + } +}