ef4a5ad1ea45eda4fde693f56f2106c553e87d93 chmalee Wed May 20 15:24:30 2026 -0700 Add cnv type to myVariants, refs #33808 diff --git src/hg/hgTracks/myVariantsTrack.c src/hg/hgTracks/myVariantsTrack.c index 51497d5b586..791e2b3d0f6 100644 --- src/hg/hgTracks/myVariantsTrack.c +++ src/hg/hgTracks/myVariantsTrack.c @@ -16,31 +16,31 @@ #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", "blockCount", "blockSizes", "chromStarts", "description", "db", "ref", "alt", "project", - "mouseover", "id", NULL + "mouseover", "itemType", "cnvType", "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); } @@ -334,109 +334,150 @@ item->score = 0; item->bin = binFromRange(item->chromStart, item->chromEnd); 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(""); + item->itemType = cloneString("snv"); + item->cnvType = 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->ref = cloneString(""); item->alt = cloneString(""); item->name = cloneString(""); item->db = database; item->description = cloneString(pos->description ? pos->description : ""); item->project = cloneString(""); item->mouseover = cloneString(""); + item->itemType = cloneString("snv"); + item->cnvType = 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; } } } } else { char *chrom = jsonStringVal(jsonFindNamedField(json, "jsonData", "chrom"), "chrom"); int chromStart = sqlUnsigned(jsonStringVal(jsonFindNamedField(json, "jsonData", "start"), "start")); int chromEnd = sqlUnsigned(jsonStringVal(jsonFindNamedField(json, "jsonData", "end"), "end")); char *name = jsonStringVal(jsonFindNamedField(json, "jsonData", "name"), "name"); int score = sqlUnsigned(jsonStringVal(jsonFindNamedField(json, "jsonData", "score"), "score")); char *strand = jsonStringVal(jsonFindNamedField(json, "jsonData", "strand"), "strand"); int thickStart = sqlUnsigned(jsonStringVal(jsonFindNamedField(json, "jsonData", "thickStart"), "thickStart")); int thickEnd = sqlUnsigned(jsonStringVal(jsonFindNamedField(json, "jsonData", "thickEnd"), "thickEnd")); 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", ""); + char *itemType = myVariantsCanonicalItemType( + jsonOptionalStringField(json, "itemType", "snv")); + if (itemType == NULL) + errAbort("invalid itemType"); + char *cnvType = ""; + if (sameString(itemType, "cnv")) + { + cnvType = myVariantsCanonicalCnvType(jsonOptionalStringField(json, "cnvType", "")); + if (cnvType == NULL) + errAbort("invalid cnvType"); + } unsigned color; htmlColorForCode(colorCode, &color); item->bin = binFromRange(chromStart, chromEnd); item->chrom = cloneString(chrom); item->chromStart = chromStart; item->chromEnd = chromEnd; + /* SNV/CNV items don't carry a thick range or BED12 blocks; force defaults + * so a stale value from a view toggle in the client can't leak through. */ + if (sameString(itemType, "snv") || sameString(itemType, "cnv")) + { + item->thickStart = chromStart; + item->thickEnd = chromEnd; + } + else + { 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); + if (sameString(itemType, "transcript")) + { + item->ref = cloneString(""); + item->alt = cloneString(""); + } + else if (sameString(itemType, "cnv")) + { + /* CNV stores the inserted/duplicated sequence in alt; ref is unused. */ + item->ref = cloneString(""); + item->alt = cloneString(alt); + } + else + { item->ref = cloneString(ref); item->alt = cloneString(alt); + } item->db = database; item->project = cloneString(project); item->mouseover = cloneString(mouseover); + item->itemType = cloneString(itemType); + item->cnvType = cloneString(cnvType); } if (!item) return; /* Parse blocks from JSON if present. Empty / missing means single full-span * block; ensureItemBlocks synthesizes that below. */ struct jsonElement *blockSizesJson = jsonFindNamedField(json, "jsonData", "blockSizes"); struct jsonElement *chromStartsJson = jsonFindNamedField(json, "jsonData", "chromStarts"); if (blockSizesJson && blockSizesJson->type == jsonList && chromStartsJson && chromStartsJson->type == jsonList) { int sizeN = slCount(blockSizesJson->val.jeList); int startN = slCount(chromStartsJson->val.jeList); if (sizeN != startN) @@ -556,30 +597,36 @@ unsigned color; if (htmlColorForCode(newVal, &color)) { struct dyString *sql = sqlDyStringCreate( "update %s set %s='%d' where id=%d", tableName, fieldName, color, id); if (isNotEmpty(scopeDb)) sqlDyStringPrintf(sql, " and db='%s'", scopeDb); if (isNotEmpty(scopeProject) && !sameString(scopeProject, "*")) sqlDyStringPrintf(sql, " and project='%s'", scopeProject); sqlUpdate(conn, sql->string); dyStringFree(&sql); cartRemove(cart, varName); } return; } +if (sameString(fieldName, "cnvType")) + { + if (isNotEmpty(newVal) && myVariantsCanonicalCnvType(newVal) == NULL) + errAbort("invalid cnvType"); + newVal = isEmpty(newVal) ? "" : myVariantsCanonicalCnvType(newVal); + } struct dyString *sql = sqlDyStringCreate("update %s set %s='%s' where id=%d", tableName, fieldName, newVal, id); if (isNotEmpty(scopeDb)) sqlDyStringPrintf(sql, " and db='%s'", scopeDb); if (isNotEmpty(scopeProject) && !sameString(scopeProject, "*")) sqlDyStringPrintf(sql, " and project='%s'", scopeProject); sqlUpdate(conn, sql->string); dyStringFree(&sql); cartRemove(cart, varName); } static void updateBlocksFields(char *trackName, struct sqlConnection *conn, char *tableName, int id, char *scopeProject, char *scopeDb) /* Pull <trackName>_blockCount, _blockSizes, _chromStarts from the cart, * validate jointly against the row's current chromStart/chromEnd via @@ -709,35 +756,38 @@ char ctVar[256]; safef(ctVar, sizeof ctVar, MYVARIANTS_FILE_VAR_PREFIX "%s", database); cartSetString(cart, ctVar, ctFile); freeMem(ctFile); } } return; } /* Handle edits. Owner edits their own table, no project/db scope filter. */ updateTextField(trackName, conn, tableName, "name", id, NULL, NULL); updateTextField(trackName, conn, tableName, "description", id, NULL, NULL); updateTextField(trackName, conn, tableName, "itemRgb", id, NULL, NULL); updateTextField(trackName, conn, tableName, "chromStart", id, NULL, NULL); updateTextField(trackName, conn, tableName, "chromEnd", id, NULL, NULL); + updateTextField(trackName, conn, tableName, "thickStart", id, NULL, NULL); + updateTextField(trackName, conn, tableName, "thickEnd", id, NULL, NULL); updateBlocksFields(trackName, conn, tableName, id, NULL, NULL); updateTextField(trackName, conn, tableName, "ref", id, NULL, NULL); updateTextField(trackName, conn, tableName, "alt", id, NULL, NULL); updateTextField(trackName, conn, tableName, "project", id, NULL, NULL); updateTextField(trackName, conn, tableName, "mouseover", id, NULL, NULL); + updateTextField(trackName, conn, tableName, "cnvType", id, NULL, NULL); /* Update any custom fields */ { char *editUserName = getUserName(); struct slName *customCols = myVariantsGetCustomFields(editUserName); struct slName *col; for (col = customCols; col != NULL; col = col->next) updateTextField(trackName, conn, tableName, col->name, id, NULL, NULL); slFreeList(&customCols); } /* Refresh the on-disk myVariants ctfile after edits. */ { char *userName = getUserName(); if (userName) { char *ctFile = myVariantsWriteCtFile(userName, database, cart); @@ -1224,34 +1274,37 @@ cartRemove(cart, varName); myVariantsShareFree(&liveShare); continue; } /* Apply field edits, scoped to the share's project/db so a recipient * cannot edit rows in projects/assemblies they were not granted. */ char *sp = liveShare->project; char *sd = liveShare->db; struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH); updateTextField(trackName, conn, tableName, "name", id, sp, sd); updateTextField(trackName, conn, tableName, "description", id, sp, sd); updateTextField(trackName, conn, tableName, "itemRgb", id, sp, sd); updateTextField(trackName, conn, tableName, "chromStart", id, sp, sd); updateTextField(trackName, conn, tableName, "chromEnd", id, sp, sd); + updateTextField(trackName, conn, tableName, "thickStart", id, sp, sd); + updateTextField(trackName, conn, tableName, "thickEnd", id, sp, sd); updateBlocksFields(trackName, conn, tableName, id, sp, sd); updateTextField(trackName, conn, tableName, "ref", id, sp, sd); updateTextField(trackName, conn, tableName, "alt", id, sp, sd); updateTextField(trackName, conn, tableName, "mouseover", id, sp, sd); + updateTextField(trackName, conn, tableName, "cnvType", id, sp, sd); struct slName *customCols = myVariantsGetCustomFields(liveShare->ownerUser); struct slName *col; for (col = customCols; col != NULL; col = col->next) updateTextField(trackName, conn, tableName, col->name, id, sp, sd); slFreeList(&customCols); hFreeConn(&conn); myVariantsShareFree(&liveShare); } hashElFreeList(&shareVars); } boolean myVariantsTrackEnabled() /* Return TRUE if the "My Variants" feature is enabled in hg.conf * and the current request comes from a valid user */ {