fc3472e7e9978eb81e3ac14e985bf27e5196ab73
kate
  Fri Jul 24 10:58:01 2020 -0700
Recommended track sets feature. refs #25601

diff --git src/hg/hgTracks/hgTracks.c src/hg/hgTracks/hgTracks.c
index 80dc532..423316e 100644
--- src/hg/hgTracks/hgTracks.c
+++ src/hg/hgTracks/hgTracks.c
@@ -60,30 +60,33 @@
 #include "search.h"
 #include "errCatch.h"
 #include "iupac.h"
 #include "botDelay.h"
 #include "chromInfo.h"
 #include "extTools.h"
 #include "basicBed.h"
 #include "customFactory.h"
 #include "genbank.h"
 #include "bigWarn.h"
 #include "wigCommon.h"
 #include "knetUdc.h"
 #include "hex.h"
 #include <openssl/sha.h>
 #include "customComposite.h"
+
+boolean isSessChanged = FALSE;
+
 //#include "bed3Sources.h"
 
 /* Other than submit and Submit all these vars should start with hgt.
  * to avoid weeding things out of other program's namespaces.
  * Because the browser is a central program, most of its cart
  * variables are not hgt. qualified.  It's a good idea if other
  * program's unique variables be qualified with a prefix though. */
 char *excludeVars[] = { "submit", "Submit", "dirty", "hgt.reset",
             "hgt.in1", "hgt.in2", "hgt.in3", "hgt.inBase",
             "hgt.out1", "hgt.out2", "hgt.out3", "hgt.out4",
             "hgt.left1", "hgt.left2", "hgt.left3",
             "hgt.right1", "hgt.right2", "hgt.right3",
             "hgt.dinkLL", "hgt.dinkLR", "hgt.dinkRL", "hgt.dinkRR",
             "hgt.tui", "hgt.hideAll", "hgt.visAllFromCt",
 	    "hgt.psOutput", "hideControls", "hgt.toggleRevCmplDisp",
@@ -7766,30 +7769,179 @@
             {
             if (jsonList == NULL)
                 jsonList = newJsonList(NULL);
 
             struct jsonElement *collection = newJsonObject(newHash(4));
             jsonObjectAdd(collection, "track", newJsonString(tdb->track));
             jsonObjectAdd(collection, "shortLabel", newJsonString(tdb->shortLabel));
             jsonListAdd(jsonList, collection);
             }
         }
     if (jsonList != NULL)
         jsonObjectAdd(jsonForClient, "collections", jsonList);
     }
 }
 
+// TODO: move to a file in lib (cheapcgi?)
+char *cgiVarStringSort(char *cgiVarString)
+/* Return cgi var string sorted alphabetically by vars */
+{
+struct slName *vars = slNameListFromString(cgiVarString, '&');
+slNameSort(&vars);
+char *cgiString = slNameListToString(vars, '&');
+return cgiString;
+}
+
+char *cgiTrackVisString(char *cgiVarString)
+/* Filter cgi var string (var=val&) to just track visibilities, but equivalence
+ * dense/pack/squish/full by replacing as 'on'.
+ * Return string with track data vars in alphabetic order */
+{
+// TODO: use cheapcgi.cgiParsedVarsNew() to parse and get list ?
+#define MAX_CGI_VARS 1000
+// NOTE: Ana featured sessions have: 473, 308, 288
+char *cgiVars[MAX_CGI_VARS];
+int ct = chopByChar(cgiVarString, '&', cgiVars, sizeof cgiVars);
+
+char *cgiVar = NULL;
+char *val = NULL;
+int i;
+struct dyString *dsCgiTrackVis = dyStringCreate("db=%s", database);
+for (i=0; i<ct; i++)
+    {
+    // TODO: attention to memory allocation
+    cgiVar = cloneString(cgiVars[i]);
+    val = rStringIn("=", cgiVar);
+    if (!val)     // expect always
+        continue;
+    val++;
+    if (sameString(val, "hide") || sameString(val, "dense") || sameString(val, "squish") ||
+        sameString(val, "pack") || sameString(val, "full") || sameString(val, "show"))
+        {
+        char *p = val;
+        *--p = 0;
+        dyStringPrintf(dsCgiTrackVis, "&%s=%s", cgiVar, sameString(val, "hide") ? "hide" : "on");
+        }
+    else if (stringIn("_sel=", cgiVar) && sameString(val, "1"))
+        {
+    val -= 5;
+    *val = 0;
+        dyStringAppend(dsCgiTrackVis, "&");
+        dyStringAppend(dsCgiTrackVis, cloneString(cgiVars[i]));
+        }
+    else
+        {
+        val = rStringIn("_imgOrd=", cgiVar);
+        if (!val)
+            val = rStringIn(".showCfg=", cgiVar);
+        if (!val)
+            continue;
+        *val = 0;
+        }
+    }
+return cgiVarStringSort(dyStringCannibalize(&dsCgiTrackVis));
+}
+
+// TODO: move to lib. Used by hgSession and hgTracks
+static void outIfNotPresent(struct cart *cart, struct dyString *dy, char *track, int tdbVis)
+/* Output default trackDb visibility if it's not mentioned in the cart. */
+{
+char *cartVis = cartOptionalString(cart, track);
+if (cartVis == NULL)
+    {
+    if (dy)
+        dyStringPrintf(dy,"&%s=%s", track, hStringFromTv(tdbVis));
+    else
+        printf("%s %s\n", track, hStringFromTv(tdbVis));
+    }
+}
+
+// TODO: move to lib. Used by hgSession and hgTracks
+static void outDefaultTracks(struct cart *cart, struct dyString *dy)
+/* Output the default trackDb visibility for all tracks
+ * in trackDb if the track is not mentioned in the cart. */
+{
+struct hash *parentHash = newHash(5);
+struct track *track;
+for (track=trackList; track; track=track->next)
+    {
+    struct trackDb *parent = track->tdb->parent;
+    if (parent)
+        {
+        if (hashLookup(parentHash, parent->track) == NULL)
+            {
+            hashStore(parentHash, parent->track);
+            if (parent->isShow)
+                outIfNotPresent(cart, dy, parent->track, tvShow);
+            }
+        }
+    if (track->tdb->visibility != tvHide)
+        outIfNotPresent(cart, dy, track->tdb->track, track->tdb->visibility);
+    }
+// Put a variable in the cart that says we put the default 
+// visibilities in it.
+if (dy)
+    dyStringPrintf(dy,"&%s=on", CART_HAS_DEFAULT_VISIBILITY);
+else
+    printf("%s on", CART_HAS_DEFAULT_VISIBILITY);
+}
+
+boolean hasSessionChanged()
+{
+/* Have any tracks been hidden or added ? */
+
+// get featured session from database
+char *sessionName = cartString(cart, "hgS_otherUserSessionName");
+if (!sessionName)
+    return FALSE;
+struct sqlConnection *conn = hConnectCentral();
+char query[1000];
+sqlSafef(query, sizeof query,
+        "SELECT contents FROM namedSessionDb where sessionName='%s'",
+                replaceChars(sessionName, " ", "%20"));
+char *cartString = sqlQuickNonemptyString(conn, query);
+hDisconnectCentral(&conn);
+
+// TODO: use cheapcgi.cgiParsedVarsNew() to parse and get list ?
+#define MAX_SESSION_LEN 100000
+char *curSessCart = (char *)needMem(MAX_SESSION_LEN);
+cgiDecodeFull(cartString, curSessCart, MAX_SESSION_LEN);
+char *curSessVisTracks = cgiTrackVisString(curSessCart);
+
+// get track-related vars from current cart
+struct dyString *dsCgiVars = newDyString(0);
+cartEncodeState(cart, dsCgiVars);
+outDefaultTracks(cart, dsCgiVars);
+char *this = dyStringCannibalize(&dsCgiVars);
+// TODO: again, better parsing
+char *this2 = replaceChars(this, "%2D", "-");
+char *thisSessVars = replaceChars(this2, "%2B", "+");
+char *thisSessVisTracks = cgiTrackVisString(thisSessVars);
+
+//freeMem(curSessCart);
+boolean isSessChanged = FALSE;
+if (differentString(curSessVisTracks, thisSessVisTracks))
+    {
+    isSessChanged = TRUE;
+    #ifdef DEBUG
+    uglyf("<br>curSess vis tracks: %s", curSessVisTracks);
+    uglyf("<br>thsSess vis tracks: %s", thisSessVisTracks);
+    #endif
+    }
+return isSessChanged;
+}
+
 void doTrackForm(char *psOutput, struct tempName *ideoTn)
 /* Make the tracks display form with the zoom/scroll buttons and the active
  * image.  If the ideoTn parameter is not NULL, it is filled in if the
  * ideogram is created.  */
 {
 struct group *group;
 struct track *track;
 char *freezeName = NULL;
 boolean hideAll = cgiVarExists("hgt.hideAll");
 boolean defaultTracks = cgiVarExists("hgt.reset");
 boolean showedRuler = FALSE;
 boolean showTrackControls = cartUsualBoolean(cart, "trackControlsOnMain", TRUE);
 long thisTime = 0, lastTime = 0;
 
 basesPerPixel = ((float)virtWinBaseCount) / ((float)fullInsideWidth);
@@ -8250,71 +8402,103 @@
 )
     {
     for(window=windows;window;window=window->next)
 	{
 	struct track *ideoTrack = chromIdeoTrack(window->trackList);
 	if (ideoTrack)
 	    {
 	    ideoTrack->limitedVisSet = TRUE;
 	    ideoTrack->limitedVis = tvHide; /* Don't draw in main gif. */
 	    }
 	}
     }
 
 if (trackImgOnly && !ideogramToo)
     {
+    // right-click to change viz 
     makeActiveImage(trackList, psOutput);
     fflush(stdout);
     return;  // bail out b/c we are done
     }
 
 if (!hideControls)
     {
     /* set white-space to nowrap to prevent buttons from wrapping when screen is
      * narrow */
     hPrintf("<DIV STYLE=\"white-space:nowrap;\">\n");
     printMenuBar();
     //menuBarAppendExtTools();
 
-    /* Show title . */
+    /* Show title */
     freezeName = hFreezeFromDb(database);
     if(freezeName == NULL)
         freezeName = "Unknown";
     hPrintf("<span style='font-size:x-large;'><B>");
     if (startsWith("zoo",database) )
         {
 	hPrintf("%s %s on %s June 2002 Assembly %s target1",
 	    organization, browserName, organism, freezeName);
 	}
     else
 	{
 	if (sameString(organism, "Archaea"))
 	    {
 	    hPrintf("%s %s on Archaeon %s Assembly",
 		organization, browserName, freezeName);
 	    }
 	else
 	    {
 	    if (stringIn(database, freezeName))
 		hPrintf("%s %s on %s %s Assembly",
 			organization, browserName, organism, freezeName);
 	    else
 		hPrintf("%s %s on %s %s Assembly (%s)",
 			organization, browserName, trackHubSkipHubName(organism), freezeName, trackHubSkipHubName(database));
 	    }
 	}
-    hPrintf("</B></span><BR>\n");
+    hPrintf("</B></SPAN>");
+
+    if (defaultTracks || hideAll)
+        cartRemove(cart, "hgS_otherUserSessionLabel");
+    char *sessionLabel = cartOptionalString(cart, "hgS_otherUserSessionLabel");
+    if (sessionLabel)
+        {
+        char *panel = "recTrackSetsPanel";
+        isSessChanged = hasSessionChanged();
+
+        struct dyString *hoverText = dyStringNew(0);
+        dyStringPrintf(hoverText, "Your browser is displaying the %s track set%s. "
+                                " Click to change to another.", sessionLabel,
+                                isSessChanged ? 
+                                ", with changes (added or removed tracks) you have requested" : "");
+        // TODO: cleanup layout tweaking for FF on IE10
+        hPrintf("&nbsp;&nbsp;&nbsp;&nbsp;");
+        hPrintf("<span id='spacer' style='display: inline; padding-left: 10px;' >&nbsp;</span>");
+
+        hPrintf("<span id='%s' class='gbSessionLabelPanel' style='display: inline-block;' title='%s'>",
+                        panel, dyStringCannibalize(&hoverText));
+        hPrintf("<span id='recTrackSetLabel' class='gbSessionLabelText gbSessionChangeIndicator %s' "
+                        "style='margin-right: 3px;'>%s</span>",
+                        isSessChanged ? "gbSessionChanged" : "", sessionLabel);
+        hPrintf("<i id='removeSessionPanel' title='Close' class='fa fa-remove' "
+                        "style='color: #a9a9a9; font-size:smaller; vertical-align: super;'></i>");
+        hPrintf("</span>");
+
+        jsOnEventById("click", "recTrackSetLabel", "showRecTrackSetsPopup(); return false;");
+        jsOnEventById("click", "removeSessionPanel", "removeSessionPanel(); return false;");
+        }
+    hPrintf("<BR>\n");
 
     /* This is a clear submit button that browsers will use by default when enter is pressed in position box. */
     hPrintf("<INPUT TYPE=IMAGE BORDER=0 NAME=\"hgt.dummyEnterButton\" src=\"../images/DOT.gif\">");
     /* Put up scroll and zoom controls. */
 #ifndef USE_NAVIGATION_LINKS
     hWrites("move ");
     hButtonWithOnClick("hgt.left3", "<<<", "move 95% to the left",
                        "return imageV2.navigateButtonClick(this);");
     hButtonWithOnClick("hgt.left2", " <<", "move 47.5% to the left",
                        "return imageV2.navigateButtonClick(this);");
     hButtonWithOnClick("hgt.left1", " < ", "move 10% to the left",
                        "return imageV2.navigateButtonClick(this);");
     hButtonWithOnClick("hgt.right1", " > ", "move 10% to the right",
                        "return imageV2.navigateButtonClick(this);");
     hButtonWithOnClick("hgt.right2", ">> ", "move 47.5% to the right",
@@ -8475,30 +8659,31 @@
 hPrintf("<td width='80' align='right'><a href='?hgt.out3=1' "
         "title='zoom out 10x'>&lt;&lt;&lt;&nbsp;&gt;&gt;&gt;</a>\n");
 hPrintf("<td width='80' align='right'><a href='?hgt.out4=1' "
         "title='zoom out 100x'>&lt;&lt;&lt;&nbsp;&gt;&gt;&gt;</a>\n");
 hPrintf("<td>&nbsp;</td>\n"); // Without width cell expands table width, forcing others to sides
 hPrintf("<td width='20' align='right'><a href='?hgt.right1=1' "
         "title='move 10&#37; to the right'>&gt;</a>\n");
 
 hPrintf("<td width='30' align='right'><a href='?hgt.right2=1' "
         "title='move 47.5&#37; to the right'>&gt;&gt;</a>\n");
 hPrintf("<td width='40' align='right'><a href='?hgt.right3=1' """
         "title='move 95&#37; to the right'>&gt;&gt;&gt;</a>\n");
 hPrintf("</tr></table>\n");
 #endif///def USE_NAVIGATION_LINKS
 
+
 /* Make clickable image and map. */
 makeActiveImage(trackList, psOutput);
 fflush(stdout);
 
 if (trackImgOnly)
     {
     // bail out b/c we are done
     if (measureTiming)
         {
         printTrackTiming();
         }
     return;
     }
 
 if (!hideControls)
@@ -9670,31 +9855,32 @@
 lastDbPosSaveCartSetting("virtModeType");
 lastDbPosSaveCartSetting("lastVirtModeType");
 lastDbPosSaveCartSetting("lastVirtModeExtraState");
 
 cartSetDbPosition(cart, database, lastDbPosCart);
 
 if (cartUsualBoolean(cart, "hgt.psOutput", FALSE))
     handlePostscript();
 else
     doTrackForm(NULL, NULL);
 
 boolean gotExtTools = extToolsEnabled();
 setupHotkeys(gotExtTools);
 if (gotExtTools)
     printExtMenuData(chromName);
-
+if (recTrackSetsEnabled())
+    printRecTrackSets();
 }
 
 void chromInfoTotalRow(int count, long long total)
 /* Make table row with total number of sequences and size from chromInfo. */
 {
 cgiSimpleTableRowStart();
 cgiSimpleTableFieldStart();
 printf("Total: %d", count);
 cgiTableFieldEnd();
 cgiSimpleTableFieldStart();
 printLongWithCommas(stdout, total);
 cgiTableFieldEnd();
 cgiTableRowEnd();
 }