bdd18de186c0f34406ca395e7ba9b1b88980587d
chmalee
  Mon Sep 23 16:44:07 2019 -0700
Optimizing the jstree on hgHubConnect by writing out a javascript object and letting ajax load the nodes of the tree on an as-needed basis. Also adding measureTiming to hgHubConnect, refs #23812

diff --git src/hg/hgHubConnect/hgHubConnect.c src/hg/hgHubConnect/hgHubConnect.c
index 726b7af..f7b23fa 100644
--- src/hg/hgHubConnect/hgHubConnect.c
+++ src/hg/hgHubConnect/hgHubConnect.c
@@ -375,46 +375,45 @@
 	ourPrintCell("");
 
     puts("</tr>");
     }
 
 printf("</tbody></TABLE>\n");
 printf("</div>");
 }
 
 void doValidateNewHub(char *hubUrl)
 /* Run hubCheck on a hub. */
 {
 struct dyString *cmd = dyStringNew(0);
 udcSetCacheTimeout(1);
 dyStringPrintf(cmd, "loader/hubCheck -htmlOut -noTracks %s", hubUrl);
-printf("<tr><td>");
-printf("running command: '%s'\n", cmd->string);
-printf("</td></tr></table>");
-printf("<div id=\"tracks\" class=\"hubTdbTree\" style=\"overflow: auto\"></div>");
+printf("</table>");
+printf("<div id=\"validateHubResult\" class=\"hubTdbTree\" style=\"overflow: auto\"></div>");
 FILE *f = popen(cmd->string, "r");
 if (f == NULL)
     errAbort("popen: error running command: \"%s\"", cmd->string);
 char buf[1024];
 jsInline("trackData = [];");
 while (fgets(buf, sizeof(buf), f))
     {
     jsInlineF("%s", buf);
     }
 if (pclose(f) == -1)
     errAbort("pclose: error for command \"%s\"", cmd->string);
-jsInline("hgCollection.init();");
+// the 'false' below prevents a few hub-search specific jstree configuration options
+jsInline("hubSearchTree.init(false);");
 dyStringFree(&cmd);
 }
 
 void hgHubConnectValidateNewHub()
 {
 // put out the top of our page
 char *hubUrl = cartOptionalString(cart, "validateHubUrl");
 printf("<div id=\"validateHub\" class=\"hubList\"> \n"
     "<table id=\"validateHub\"> \n"
     "<thead><tr> \n");
 printf("<th colspan=\"6\" id=\"addHubBar\"><label for=\"validateHubUrl\">URL:</label> \n");
 if (hubUrl != NULL)
     {
 	printf("<input name=\"validateText\" id=\"validateHubUrl\" class=\"hubField\" "
 	    "type=\"text\" size=\"65\" value=\"%s\"> \n", hubUrl);
@@ -1060,128 +1059,291 @@
             if (hst->textLength == hubSearchTextLong)
                 dyStringPrintf(tdbOut->descriptionMatch, "%s", hst->text);
             else if (hst->textLength == hubSearchTextMeta)
                 {
                 if (isNotEmpty(dyStringContents(tdbOut->metaTags)))
                     dyStringPrintf(tdbOut->metaTags, ", %s", hst->text);
                 else
                     dyStringPrintf(tdbOut->metaTags, "%s", hst->text);
                 }
             }
         }
     }
 return hubOut;
 }
 
+static char *tdbOutputStructureLabelToId(struct tdbOutputStructure *tdbOut)
+/* Make an array name out of a tdbOutputStruct */
+{
+struct dyString *id = dyStringNew(0);
+dyStringPrintf(id, "%s", htmlEncode(dyStringContents(tdbOut->shortLabel)));
+if (tdbOut->childCount > 0)
+    {
+    dyStringPrintf(id, " (%d subtrack%s)", tdbOut->childCount,
+        tdbOut->childCount == 1 ? "" : "s");
+    }
+return dyStringCannibalize(&id);
+}
+
+static void printTdbOutputStructureToDyString(struct tdbOutputStructure *tdbOut, struct dyString *dy, char *arrayName)
+/* Print a tdbOutputStructure to a dyString*/
+{
+dyStringPrintf(dy, "trackData['%s'] = [", arrayName);
+
+if (tdbOut->childCount > 0)
+    {
+    struct dyString *subtrackDy = dyStringNew(0);
+    struct tdbOutputStructure *child = tdbOut->children;
+    while (child != NULL)
+        {
+        char *childId = tdbOutputStructureLabelToId(child);
+        dyStringPrintf(dy, "\n\t{\n\tid: '%s',\n\tparent: '%s',\n\t"
+            "li_attr: {nodetype:'track', configlink:'%s'},\n\ttext: \'%s ",
+            childId, arrayName, dyStringContents(child->configUrl), childId);
+        if (isNotEmpty(dyStringContents(child->metaTags)))
+            {
+            dyStringPrintf(dy, "<br><span class=\\'descriptionMatch\\'><em>Metadata: %s</em></span>",
+                htmlEncode(dyStringContents(child->metaTags)));
+            }
+        if (isNotEmpty(dyStringContents(child->descriptionMatch)))
+            {
+            dyStringPrintf(dy, "<br><span class=\\'descriptionMatch\\'><em>Description: %s</em></span>",
+                htmlEncode(dyStringContents(child->descriptionMatch)));
+            }
+        dyStringPrintf(dy, "\'");
+        if (child->childCount > 0)
+            {
+            dyStringPrintf(dy, ",\n\tchildren: true");
+            printTdbOutputStructureToDyString(child, subtrackDy, childId);
+            }
+        dyStringPrintf(dy, "\n\t},");
+        child = child->next;
+        }
+    dyStringPrintf(dy, "];\n");
+    if (isNotEmpty(dyStringContents(subtrackDy)))
+        dyStringPrintf(dy, "%s", subtrackDy->string);
+    }
+else
+    dyStringPrintf(dy, "];\n");
+}
+
+
+void printGenomeOutputStructureToDyString(struct genomeOutputStructure *genomeOut, struct dyString *dy, char *genomeNameId)
+/* Print a genomeOutputStructure to a dyString */
+{
+struct tdbOutputStructure *tdbOut = NULL;
+static  struct dyString *tdbArrayDy = NULL; // the dyString for all of the tdb objects
+static struct dyString *idString = NULL; // the special id of this track
+if (tdbArrayDy == NULL)
+    tdbArrayDy = dyStringNew(0);
+if (idString == NULL)
+    idString = dyStringNew(0);
+
+// The structure here is:
+// trackData[genome] = [{track 1 obj}, {track2 obj}, {track3 obj}, ... ]
+// trackData[track1] = [{search hit text}, {subtrack1 search hit}, {subtrack2 search hit}, ... ]
+//
+// if track1, track2, track3 are container tracks, then the recursive function
+// tdbOutputStructureToDystring creates the above trackData[track1] = [{}] for 
+// each of the containers, otherwise a single child of the genome is sufficient
+dyStringPrintf(dy, "trackData['%s'] = [", genomeNameId);
+if (genomeOut->tracks != NULL)
+    {
+    tdbOut = genomeOut->tracks;
+    while (tdbOut != NULL)
+        {
+        dyStringPrintf(idString, "%s", tdbOutputStructureLabelToId(tdbOut));
+        dyStringPrintf(dy, "\n\t{\n\t'id': '%s',\n\t'parent': '%s',\n\t"
+            "'li_attr': {'nodetype':'track', configlink: '%s'},\n\t'text': \'%s ",
+            idString->string, genomeNameId, dyStringContents(tdbOut->configUrl), idString->string);
+        if (isNotEmpty(dyStringContents(tdbOut->metaTags)))
+            {
+            dyStringPrintf(dy, "<br><span class=\\'descriptionMatch\\'><em>Metadata: %s</em></span>",
+                htmlEncode(dyStringContents(tdbOut->metaTags)));
+            }
+        if (isNotEmpty(dyStringContents(tdbOut->descriptionMatch)))
+            {
+            dyStringPrintf(dy, "<br><span class=\\'descriptionMatch\\'><em>Description: %s</em></span>",
+                htmlEncode(dyStringContents(tdbOut->descriptionMatch)));
+            }
+        dyStringPrintf(dy, "\'");
+
+        // above we took care of both non-heirarchical tracks and the top-level containers,
+        // now do container children, which also takes care of any deeper heirarchies
+        if (tdbOut->childCount > 0)
+            dyStringPrintf(dy, ",\n\t'children': true");
+        dyStringPrintf(dy, "\n\t},\n");
+
+        if (tdbOut->childCount > 0)
+            printTdbOutputStructureToDyString(tdbOut, tdbArrayDy, idString->string);
+        tdbOut = tdbOut->next;
+        dyStringClear(idString);
+        }
+    }
+dyStringPrintf(dy, "];\n"); // close off genome node
+dyStringPrintf(dy, "%s\n", tdbArrayDy->string);
+dyStringClear(tdbArrayDy);
+dyStringClear(idString);
+}
+
+void printHubOutputStructure(struct hubOutputStructure *hubOut, struct hubEntry *hubInfo)
+/* Convert a hubOutputStructure to a jstree-readable string */
+{
+struct dyString *dy = dyStringNew(0);
+// The leading '#' tells the javascript this is a 'root' node
+dyStringPrintf(dy, "trackData['#_%d'] = [", hubInfo->id);
+if (isNotEmpty(dyStringContents(hubOut->descriptionMatch)))
+    {
+    dyStringPrintf(dy, "{'id':'%d_descriptionMatchText','parent':'#_%d',"
+        "'state':{'opened': true},'text': 'Hub Description: "
+        "<span class=\"descriptionMatch\"><em>%s</em></span>'},",
+        hubInfo->id, hubInfo->id, dyStringContents(hubOut->descriptionMatch));
+    }
+struct genomeOutputStructure *genomeOut = hubOut->genomes;
+struct dyString *genomeDy = dyStringNew(0);
+if (genomeOut != NULL)
+    {
+    dyStringPrintf(dy, "{'id':'%d_assemblies', 'text':'%d Matching Assembl%s', 'parent':'#_%d', "
+        "'children':true, 'li_attr': {'state':{'opened': 'false'}}}];\n",
+        hubInfo->id, hubOut->genomeCount, hubOut->genomeCount == 1 ? "y" : "ies", hubInfo->id);
+    dyStringPrintf(dy, "trackData['%d_assemblies'] = [", hubInfo->id);
+
+    while (genomeOut != NULL)
+        {
+        char *assemblyName = htmlEncode(dyStringContents(genomeOut->shortLabel));
+        char genomeNameId[512];
+        safef(genomeNameId, sizeof(genomeNameId), "%d_%s", hubInfo->id, assemblyName);
+        dyStringPrintf(dy, "{'id': '%s', 'parent': '%d_assemblies', 'children': true, "
+            "'li_attr': {'assemblylink': '%s','nodetype': 'assembly'},"
+            "'text': \"%s",
+            genomeNameId, hubInfo->id, dyStringContents(genomeOut->assemblyLink), assemblyName);
+        if (genomeOut->trackCount > 0)
+            {
+            dyStringPrintf(dy, " (%d track%s) ", genomeOut->trackCount,
+                genomeOut->trackCount == 1 ? "" : "s");
+            }
+        if (isNotEmpty(dyStringContents(genomeOut->metaTags)))
+            {
+            dyStringPrintf(dy, "<br><span class='descriptionMatch'><em>%s</em></span>",
+                htmlEncode(dyStringContents(genomeOut->metaTags)));
+            }
+        if (isNotEmpty(dyStringContents(genomeOut->descriptionMatch)))
+            {
+            dyStringPrintf(dy, "<br><em>Assembly Description:</em> %s",
+                htmlEncode(dyStringContents(genomeOut->descriptionMatch)));
+            }
+        dyStringPrintf(dy, "\"},");
+        printGenomeOutputStructureToDyString(genomeOut, genomeDy, genomeNameId);
+        genomeOut = genomeOut->next;
+        }
+    }
+dyStringPrintf(dy, "];\n");
+dyStringPrintf(dy, "%s", genomeDy->string);
+jsInline(dy->string);
+dyStringClear(dy);
+}
 
 static void printOutputForHub(struct hubEntry *hubInfo, struct hubSearchText *hubSearchResult, int count)
 /* Given a hub's info and a structure listing the search hits within the hub, first print
  * a basic line of hub information with a "connect" button.  Then, if the search results
  * are non-NULL, write out information about the genomes and tracks from the search hits that
  * match the db filter.
  * If there are no search results to print, the basic hub lines are combined into a single HTML table
  * that is defined outside this function.
  * Otherwise, each hub line is printed in its own table followed by a <ul> containing details
  * about the search results. */
 {
 if (hubSearchResult != NULL)
     printf("<table class='hubList'><tbody>\n");
 outputPublicTableRow(hubInfo, count);
 if (hubSearchResult != NULL)
     {
     printf("</tbody></table>\n");
+    printf("<div class=\"hubTdbTree\">\n");
+    printf("<div id='tracks%d'></div>", hubInfo->id); // div for the jstree for this hub's search result(s)
+    printf("</div>\n");
     struct trackHub *hub = fetchTrackHub(hubInfo);
     struct hubOutputStructure *hubOut = buildHubSearchOutputStructure(hub, hubSearchResult);
     if (dyStringIsEmpty(hubOut->descriptionMatch) && (hubOut->genomes == NULL))
         return; // no detailed search results; hit must have been to hub short label or something   
-
-    printf("<div class=\"hubTdbTree\">\n");
-    printf("<ul>\n");
-    printf("<li>Search details ...\n<ul>\n");
-    if (isNotEmpty(dyStringContents(hubOut->descriptionMatch)))
-        printf("<li>Hub Description:&nbsp<span class='descriptionMatch'><em>%s</em></span></li>\n", dyStringContents(hubOut->descriptionMatch));
-
-    struct genomeOutputStructure *genomeOut = hubOut->genomes;
-    if (genomeOut != NULL)
-        {
-        printf("<li>%d Matching Assembl%s\n<ul>\n", hubOut->genomeCount, hubOut->genomeCount==1?"y":"ies");
-        while (genomeOut != NULL)
-            {
-            printSearchOutputForGenome(genomeOut);
-            genomeOut = genomeOut->next;
-            }
-        printf("</ul></li>\n");
-        }
-    printf("</ul></li></ul></div>\n");
+    printHubOutputStructure(hubOut, hubInfo);
     }
 }
 
 int hubEntryCmp(const void *va, const void *vb)
 /* Compare to sort based on shortLabel */
 {
 const struct hubEntry *a = *((struct hubEntry **)va);
 const struct hubEntry *b = *((struct hubEntry **)vb);
 
 return strcasecmp(a->shortLabel, b->shortLabel);
 }
 
 
 void printHubList(struct slName *hubsToPrint, struct hash *hubLookup, struct hash *searchResultHash)
 /* Print out a list of hubs, possibly along with search hits to those hubs.
  * hubLookup takes hub URLs to struct hubEntry
  * searchResultHash takes hub URLs to struct hubSearchText * (list of hits on that hub)
  */
 {
 int count = 0;
 int udcTimeoutVal = udcCacheTimeout();
 char *udcOldDir = cloneString(udcDefaultDir());
 char *searchUdcDir = cfgOptionDefault("hgHubConnect.cacheDir", udcOldDir);
 udcSetDefaultDir(searchUdcDir);
 udcSetCacheTimeout(1<<30);
 struct hubEntry *hubList = NULL;
 struct hubEntry *hubInfo;
+long slTime;
+long printOutputForHubTime;
+boolean measureTiming = cartUsualBoolean(cart, "measureTiming", FALSE);
 if (hubsToPrint != NULL)
     {
     printHubListHeader();
 
     if (searchResultHash == NULL) // if not displaying search results, join the hub <tr>s into one table
         printf("<table class='hubList'><tbody>\n");
     struct slName *thisHubName = NULL;
     for (thisHubName = hubsToPrint; thisHubName != NULL; thisHubName = thisHubName->next)
         {
         hubInfo = (struct hubEntry *) hashFindVal(hubLookup, thisHubName->name);
         if (hubInfo == NULL)
             {
             /* This shouldn't happen often, but maybe the search hits list was built from an outdated
              * search text file that includes hubs for which no info is available.
              * Skip this hub. */
             continue;
             }
         slAddHead(&hubList, hubInfo);
         }
     slSort(&hubList, hubEntryCmp);
+    slTime = clock1000();
 
+    jsInline("trackData = [];\n");
     for (hubInfo = hubList; hubInfo != NULL; hubInfo = hubInfo->next)
         {
         struct hubSearchText *searchResult = NULL;
         if (searchResultHash != NULL)
             {
             searchResult = (struct hubSearchText *) hashMustFindVal(searchResultHash, hubInfo->hubUrl);
             }
         printOutputForHub(hubInfo, searchResult, count);
         count++;
         }
+    printOutputForHubTime = clock1000();
+    if (measureTiming)
+        printf("hgHubConnect: printOutputForHubTime before js execution: %lu millis<BR>\n", printOutputForHubTime - slTime);
     if (searchResultHash == NULL)
         printf("</tbody></table>\n");
     }
 udcSetCacheTimeout(udcTimeoutVal);
 udcSetDefaultDir(udcOldDir);
 if (hubsToPrint != NULL)
     {
     /* Write out the list of hubs in a single table inside a div that will be hidden by
      * javascript.  This table is used (before being hidden) to set common column widths for
      * the individual hub tables when they're split by detailed search results. */
     printf("<div id='hideThisDiv'>\n");
     printf("<table class='hubList' id='hideThisTable'><tbody>\n");
     for (hubInfo = hubList; hubInfo != NULL; hubInfo = hubInfo->next)
         {
         printOutputForHub(hubInfo, NULL, count);
@@ -1482,31 +1644,30 @@
 cartWebStart(cart, NULL, "%s", pageTitle);
 
 printf(
 "<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.7/themes/default/style.min.css\" />\n"
 "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.1/jquery.min.js\"></script>\n"
 "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.7/jstree.min.js\"></script>\n"
 "<style>.jstree-default .jstree-anchor { height: initial; } </style>\n"
 );
 jsIncludeFile("utils.js", NULL);
 jsIncludeFile("jquery-ui.js", NULL);
 webIncludeResourceFile("jquery-ui.css");
 jsIncludeFile("ajax.js", NULL);
 jsIncludeFile("hgHubConnect.js", NULL);
 webIncludeResourceFile("hgHubConnect.css");
 jsIncludeFile("jquery.cookie.js", NULL);
-jsIncludeFile("hgCollection.js", NULL);
 jsIncludeFile("spectrum.min.js", NULL);
 
 printf("<div id=\"hgHubConnectUI\"> <div id=\"description\"> \n");
 printf(
     "<P>Track data hubs are collections of external tracks that can be imported into the UCSC Genome Browser. "
     "Hub tracks show up under the hub's own blue label bar on the main browser page, "
     "as well as on the configure page. For more information, including where to "
     "<A HREF=\"../goldenPath/help/hgTrackHubHelp.html#Hosting\">host</A> your track hub, see the "
     "<A HREF=\"../goldenPath/help/hgTrackHubHelp.html\" TARGET=_blank>"
     "User's Guide</A>."
     "To import a public hub click its \"Connect\" button below.</P>"
     "<P><B>NOTE: Because Track Hubs are created and maintained by external sources,"
     " UCSC is not responsible for their content.</B></P>"
    );
 printf("</div>\n");
@@ -1595,83 +1756,40 @@
     hgHubConnectValidateNewHub();
 printf("</div>");
 
 printf("<div class=\"tabFooter\">");
 
 printf("<span class=\"small\">"
     "Contact <a href=\"../contacts.html\">us</A> to add a public hub."
     "</span>\n");
 printf("</div>");
 
 cgiMakeHiddenVar(hgHubConnectRemakeTrackHub, "on");
 
 puts("</FORM>");
 printf("</div>\n");
 
-jsInline(
-"var hubSearchTree = (function() {\n"
-"    // Effectively global vars set by init\n"
-"    var treeDiv;        // Points to div we live in\n"
-"\n"
-"    function hubSearchTreeContextMenuHandler (node, callback) {\n"
-"        var nodeType = node.li_attr.nodetype;\n"
-"        if (nodeType == 'track') {\n"
-"            callback({\n"
-"               'openConfig': {\n"
-"                   'label' : 'Configure this track',\n"
-"                   'action' : function () {window.open(node.li_attr.configlink, '_blank'); }\n"
-"               }\n"
-"            });\n"
-"        }\n"
-"        else if (nodeType == 'assembly') {\n"
-"            callback({\n"
-"               'openConfig': {\n"
-"                   'label' : 'Open this assembly',\n"
-"                   'action' : function () {window.open(node.li_attr.assemblylink, '_blank'); }\n"
-"               }\n"
-"            });\n"
-"        }\n"
-"    }\n"
-"    function toggleExpansion(node, event) {\n"
-"       var ident = '#' + node.id;\n"
-"       if (event.type != 'contextmenu')\n"
-"           $(ident).jstree(true).toggle_node(node);\n"
-"       return false;\n"
-"    }\n" 
-"    function init() {\n"
-"       $.jstree.defaults.core.themes.icons = false;\n"
-"       $.jstree.defaults.core.themes.dots = true;\n"
-"       $.jstree.defaults.contextmenu.show_at_node = false;\n"
-"       $.jstree.defaults.contextmenu.items = hubSearchTreeContextMenuHandler\n"
-"       treeDiv=$('.hubTdbTree');\n"
-"       treeDiv.jstree({\n"
-"               'conditionalselect' : function (node, event) { toggleExpansion(node, event); },\n"
-"               'plugins' : ['conditionalselect', 'contextmenu'],\n"
-"               'core' : { dblclick_toggle: false }\n"
-"               });\n"
-"    }\n\n"
-"    return { init: init};\n\n"
-"}());\n"
-"\n"
-"$(function () {\n"
-"    hubSearchTree.init();\n"
+jsInline("$(function () {\n"
+"    console.time(\"init time\");\n"
+"    hubSearchTree.init(true);\n"
+"    console.timeEnd(\"init time\");\n"
 "});\n"
 );
 
 cartWebEnd();
 }
 
-char *excludeVars[] = {"Submit", "submit", "hc_one_url", 
+char *excludeVars[] = {"Submit", "submit", "hc_one_url", "validateHubUrl",
     hgHubCheckUrl, hgHubDoClear, hgHubDoDisconnect,hgHubDoRedirect, hgHubDataText, 
     hgHubConnectRemakeTrackHub, NULL};
 
 int main(int argc, char *argv[])
 /* Process command line. */
 {
 long enteredMainTime = clock1000();
 oldVars = hashNew(10);
 cgiSpoof(&argc, argv);
 cartEmptyShell(doMiddle, hUserCookie(), excludeVars, oldVars);
 cgiExitTime("hgHubConnect", enteredMainTime);
 return 0;
 }