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