e7264355c83a0d0875b372b7ad26162adb363f6c chmalee Tue May 12 13:20:28 2026 -0700 myVariants v1 follow-ups: CT edit gate, anon-user button gate, hgc duplicate link, item-name synthesis from ref/alt or HGVS, refs #33808 diff --git src/hg/hgTracks/myVariantsTrack.c src/hg/hgTracks/myVariantsTrack.c index 016a79c73c1..3076c344944 100644 --- src/hg/hgTracks/myVariantsTrack.c +++ src/hg/hgTracks/myVariantsTrack.c @@ -9,38 +9,154 @@ #include "linefile.h" #include "jksql.h" #include "hdb.h" #include "hgTracks.h" #include "bed.h" #include "binRange.h" #include "myVariants.h" #include "myVariantsShare.h" #include "jsonParse.h" #include "sqlNum.h" #include "customFactory.h" #include "hgConfig.h" #include "htmlColor.h" #include "wikiLink.h" #include "hgFind.h" +#include "hgHgvs.h" #include "jsonWrite.h" static char *reservedFieldNames[] = { "bin", "chrom", "chromStart", "chromEnd", "name", "score", "strand", "thickStart", "thickEnd", "itemRgb", "description", "db", "ref", "alt", "project", "mouseover", "id", NULL }; +static char *truncateSeq(char *seq, int maxLen) +/* Return seq cloned and truncated to maxLen with "..." appended if longer. + * NULL or empty input returns NULL. */ +{ +if (isEmpty(seq)) + return NULL; +if (strlen(seq) <= (size_t)maxLen) + return cloneString(seq); +struct dyString *dy = dyStringNew(maxLen + 4); +dyStringAppendN(dy, seq, maxLen); +dyStringAppend(dy, "..."); +return dyStringCannibalize(&dy); +} + +static char *changeTypePrefix(enum hgvsChangeType t) +/* Map a non-substitution HGVS change type to its short label. */ +{ +switch (t) + { + case hgvsctDel: return "Del"; + case hgvsctDup: return "Dup"; + case hgvsctIns: return "Ins"; + case hgvsctInv: return "Inv"; + case hgvsctCon: return "Con"; + default: return NULL; + } +} + +static char *synthesizeItemName(char *ref, char *alt, + enum hgvsChangeType changeType, char *changeSeq) +/* Build a default name for a new myVariants item from its ref/alt or HGVS + * change. Returns a cloned string, or NULL when the row's auto-increment + * id is needed (the SQL layer fills in "Variant N" after the INSERT). */ +{ +boolean haveRef = !isEmpty(ref); +boolean haveAlt = !isEmpty(alt); +struct dyString *dy = dyStringNew(0); +if (haveRef && haveAlt) + dyStringPrintf(dy, "%s>%s", ref, alt); +else if (haveRef) + dyStringPrintf(dy, "Ref: %s", ref); +else if (haveAlt) + dyStringPrintf(dy, "Alt: %s", alt); +else + { + char *prefix = changeTypePrefix(changeType); + if (prefix != NULL && !isEmpty(changeSeq)) + { + char *trunc = truncateSeq(changeSeq, 10); + dyStringPrintf(dy, "%s: %s", prefix, trunc); + freeMem(trunc); + } + } +if (dyStringIsEmpty(dy)) + { + dyStringFree(&dy); + return NULL; + } +return dyStringCannibalize(&dy); +} + +static void extractHgvsChange(char *hgvsTerm, + char **retRef, char **retAlt, + enum hgvsChangeType *retType, char **retSeq) +/* Parse an HGVS term and pull either ref/alt (substitution) or the change + * type plus its asserted sequence (del/dup/ins/inv/con). Each out parameter + * is set to a freshly cloned string or to NULL/hgvsctUndefined when the + * piece is unavailable. */ +{ +*retRef = NULL; +*retAlt = NULL; +*retType = hgvsctUndefined; +*retSeq = NULL; +if (isEmpty(hgvsTerm)) + return; +struct hgvsVariant *hgvs = hgvsParseTerm(hgvsTerm); +if (hgvs == NULL || isEmpty(hgvs->changes)) + return; +struct dyString *dyError = dyStringNew(0); +struct hgvsChange *change = hgvsParseNucleotideChange(hgvs->changes, hgvs->type, dyError); +dyStringFree(&dyError); +if (change == NULL) + return; +if (change->type == hgvsctSubst && + change->value.refAlt.altType == hgvsstSimple) + { + if (!isEmpty(change->value.refAlt.refSequence)) + *retRef = cloneString(change->value.refAlt.refSequence); + char *altSeq = change->value.refAlt.altValue.seq; + if (!isEmpty(altSeq)) + *retAlt = cloneString(altSeq); + } +else + { + *retType = change->type; + switch (change->type) + { + case hgvsctDel: + case hgvsctDup: + case hgvsctInv: + if (!isEmpty(change->value.refAlt.refSequence)) + *retSeq = cloneString(change->value.refAlt.refSequence); + break; + case hgvsctIns: + case hgvsctCon: + if (change->value.refAlt.altType == hgvsstSimple && + !isEmpty(change->value.refAlt.altValue.seq)) + *retSeq = cloneString(change->value.refAlt.altValue.seq); + break; + default: + break; + } + } +} + static boolean isValidFieldName(char *name) /* Validate that name matches [a-zA-Z_][a-zA-Z0-9_]*, is not a reserved column name, * and does not start with _hidden_ */ { if (isEmpty(name)) return FALSE; if (startsWith("_hidden_", name)) return FALSE; /* Check first char is letter or underscore */ if (!isalpha(name[0]) && name[0] != '_') return FALSE; /* Check remaining chars are alphanumeric or underscore */ int i; for (i = 1; name[i] != '\0'; i++) { @@ -55,31 +171,34 @@ return FALSE; } return TRUE; } void myVariantsJsCommand(char *command, struct track *trackList, struct hash *trackHash) /* Execute some command sent to us from the javaScript. All we know for sure is that * the first word of the command is "myVariants." We expect it to be of format: * myVariants <trackName> <jsonData> * where jsonData is a JSON string containing the item details */ { if (!cfgOptionBooleanDefault("doMyVariants", FALSE)) return; char *userName = getUserName(); if (userName == NULL) + { + warn("You must be logged in to add an annotation."); return; + } /* This command runs mysql commands so require the hgsids match to prevent CSRF. */ char *suppliedHgsid = cgiOptionalString("hgsid"); char *expectedHgsid = cartSessionId(cart); if (isEmpty(suppliedHgsid) || isEmpty(expectedHgsid) || !sameString(suppliedHgsid, expectedHgsid)) errAbort("session token missing or invalid"); /* Parse out command into local variables. */ char *words[3]; char *dupeCommand = cloneString(command); /* For parsing. */ int wordCount = chopByWhiteRespectDoubleQuotes(dupeCommand, words, ArraySize(words)); if (wordCount != 3) errAbort("Expecting %d words in jsCommand '%s'", wordCount, command); char *jsonData = words[2]; @@ -135,56 +254,62 @@ fixSinglePos(hgp); if (hgp->singlePos) { // fill out an item AND redirect to that location somehow? item->chrom = hgp->singlePos->chrom; // matchesHgvs widens the position by HGVS_FIND_PADDING on each // side for visual context; the saved variant should be the exact // mapped span. item->chromStart = item->thickStart = hgp->singlePos->chromStart + HGVS_FIND_PADDING; item->chromEnd = item->thickEnd = hgp->singlePos->chromEnd - HGVS_FIND_PADDING; item->strand[0] = '.'; item->score = 0; item->bin = binFromRange(item->chromStart, item->chromEnd); - item->name = hgp->singlePos->name; - item->ref = cloneString(""); - item->alt = cloneString(""); + char *hgvsRef = NULL, *hgvsAlt = NULL, *changeSeq = NULL; + enum hgvsChangeType changeType = hgvsctUndefined; + extractHgvsChange(hgvs, &hgvsRef, &hgvsAlt, &changeType, &changeSeq); + item->ref = hgvsRef ? hgvsRef : cloneString(""); + item->alt = hgvsAlt ? hgvsAlt : cloneString(""); + item->name = synthesizeItemName(item->ref, item->alt, changeType, changeSeq); + if (item->name == NULL) + item->name = cloneString(""); + freeMem(changeSeq); item->db = database; item->description = cloneString(hgp->singlePos->description); item->project = cloneString(""); item->mouseover = cloneString(""); } } else { /* HGVS didn't match - try hgFind for position/gene resolution */ struct hgPositions *hgpFind = hgPositionsFind(database, hgvs, "", "myVariants", cart, FALSE, FALSE, NULL); if (hgpFind != NULL && hgpFind->posCount == 1) { struct hgPos *pos = hgpFind->singlePos; item->chrom = cloneString(pos->chrom); item->chromStart = item->thickStart = pos->chromStart; item->chromEnd = item->thickEnd = pos->chromEnd; item->strand[0] = '.'; item->score = 0; item->bin = binFromRange(item->chromStart, item->chromEnd); - item->name = cloneString(pos->name ? pos->name : hgvs); item->ref = cloneString(""); item->alt = cloneString(""); + item->name = cloneString(""); item->db = database; item->description = cloneString(pos->description ? pos->description : ""); item->project = cloneString(""); item->mouseover = cloneString(""); } else if (hgpFind != NULL && hgpFind->posCount > 1) { warn("Position '%s' matches %d locations - please be more specific", hgvs, hgpFind->posCount); return; } else { warn("Position '%s' not found", hgvs); return; } @@ -204,30 +329,37 @@ char *colorCode = jsonStringVal(jsonFindNamedField(json, "jsonData", "color"), "color"); char *description = jsonStringVal(jsonFindNamedField(json, "jsonData", "description"), "description"); char *ref = jsonOptionalStringField(json, "ref", ""); char *alt = jsonOptionalStringField(json, "alt", ""); char *project = jsonOptionalStringField(json, "project", ""); char *mouseover = jsonOptionalStringField(json, "mouseover", ""); unsigned color; htmlColorForCode(colorCode, &color); item->bin = binFromRange(chromStart, chromEnd); item->chrom = cloneString(chrom); item->chromStart = chromStart; item->chromEnd = chromEnd; item->thickStart = thickStart; item->thickEnd = thickEnd; + if (isEmpty(name)) + { + item->name = synthesizeItemName(ref, alt, hgvsctUndefined, NULL); + if (item->name == NULL) + item->name = cloneString(""); + } + else item->name = cloneString(name); item->score = score; item->strand[0] = strand[0]; item->itemRgb = color; item->description = cloneString(description); item->ref = cloneString(ref); item->alt = cloneString(alt); item->db = database; item->project = cloneString(project); item->mouseover = cloneString(mouseover); } if (!item) return; @@ -434,48 +566,46 @@ struct dyString *query = sqlDyStringCreate("select * from %s where ", tableName); hAddBinToQueryGeneral("bin", winStart, winEnd, query); sqlDyStringPrintf(query, " chrom='%s' and chromStart < %d and chromEnd > %d", chromName, winEnd, winStart); if (isNotEmpty(whereExtra)) sqlDyStringPrintf(query, " and (%-s)", whereExtra); struct sqlResult *sr = sqlGetResult(conn, query->string); dyStringFree(&query); char **row; struct dyString *mouseover = dyStringNew(0); while ((row = sqlNextRow(sr)) != NULL) { struct myVariants *item = myVariantsLoad(row); struct bed *bed; AllocVar(bed); - char buf[64]; - safef(buf, sizeof(buf), "%u %s", item->id, item->name); bed->chrom = item->chrom; bed->chromStart = item->chromStart; bed->chromEnd = item->chromEnd; - bed->name = cloneString(buf); + bed->name = cloneString(item->name); bed->score = item->score; bed->strand[0] = item->strand[0]; bed->thickStart = item->thickStart; bed->thickEnd = item->thickEnd; bed->itemRgb = item->itemRgb; lf = bedMungToLinkedFeatures(&bed, tdb, 9, 0, 1000, TRUE); dyStringClear(mouseover); if (item->mouseover && isNotEmpty(item->mouseover)) lf->mouseOver = cloneString(item->mouseover); else { - dyStringPrintf(mouseover, "%s", buf); + dyStringPrintf(mouseover, "%s", item->name); if (item->ref != NULL && isNotEmpty(item->ref)) dyStringPrintf(mouseover, "<br>Ref: %s", item->ref); if (item->alt != NULL && isNotEmpty(item->alt)) dyStringPrintf(mouseover, "<br>Alt: %s", item->alt); lf->mouseOver = cloneString(dyStringContents(mouseover)); } slAddHead(&lfList, lf); } sqlFreeResult(&sr); dyStringFree(&mouseover); return lfList; } void myVariantsLoadItems(struct track *track) /* Load up items in track already. Also make up a pseudo-item that is