b038f3dafce99493b0ca00305538c6a3dd6f041a
jcasper
  Tue Sep 26 15:58:44 2023 -0700
Initial commit of track decorators, refs #30237

diff --git src/hg/hgTracks/hgTracks.c src/hg/hgTracks/hgTracks.c
index 638d84e..0050502 100644
--- src/hg/hgTracks/hgTracks.c
+++ src/hg/hgTracks/hgTracks.c
@@ -67,30 +67,33 @@
 #include "customFactory.h"
 #include "dupTrack.h"
 #include "genbank.h"
 #include "bigWarn.h"
 #include "wigCommon.h"
 #include "knetUdc.h"
 #include "hex.h"
 #include <openssl/sha.h>
 #include "customComposite.h"
 #include "chromAlias.h"
 #include "jsonWrite.h"
 #include "cds.h"
 #include "cacheTwoBit.h"
 #include "cartJson.h"
 #include "wikiLink.h"
+#include "decorator.h"
+#include "decoratorUi.h"
+#include "mouseOver.h"
 
 //#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",
@@ -7663,30 +7666,105 @@
     return;
 struct track *subtrack;
 for (subtrack = tg->subtracks; subtrack != NULL; subtrack = subtrack->next)
     {
     if (!isSubtrackVisible(subtrack))
         continue;
     if (!hashLookup(nonEmptySubtracksHash, trackHubSkipHubName(subtrack->track)))
         {
         subtrack->loadItems = dontLoadItems;
         subtrack->limitedVis = tvHide;
         subtrack->limitedVisSet = TRUE;
         }
     }
 }
 
+
+int tdbHasDecorators(struct track *track)
+{
+struct slName *decoratorSettings = trackDbSettingsWildMatch(track->tdb, "decorator.*");
+if (decoratorSettings)
+    {
+    slNameFreeList(&decoratorSettings);
+    return 1;
+    }
+return 0;
+}
+
+
+void loadDecorators(struct track *track)
+/* Load decorations from a source (file) and put them in a decorator, then add that
+ * decorator to the list of the track's decorators.  Within the new decorator, each
+ * of the decorations should have been entered into a hash table that is indexed by the
+ * name of the linked feature.  That way when we're drawing the linked feature, we'll
+ * be able to quickly locate the associated decorations.
+ *
+ * Note that this will need amendment when multiple decoration sources are possible.
+ */
+{
+char *decoratorUrl = trackDbSetting(track->tdb, "decorator.default.bigDataUrl");
+if (!decoratorUrl)
+    {
+    errAbort ("Decorator tags are present in track %s, but no decorator file specified (decorator.default.bigDataUrl is missing)",
+            track->track);
+    return;
+    }
+
+struct bbiFile *bbi = NULL;
+struct errCatch *errCatch = errCatchNew();
+if (errCatchStart(errCatch))
+    {
+    if (isValidBigDataUrl(decoratorUrl,TRUE))
+        bbi = bigBedFileOpenAlias(decoratorUrl, chromAliasFindAliases);
+    }
+errCatchEnd(errCatch);
+if (errCatch->gotError)
+    {
+    errAbort ("Network error while attempting to retrieve decorations from %s (track %s) - %s",
+            decoratorUrl, track->track, dyStringContents(errCatch->message));
+    return;
+    }
+errCatchFree(&errCatch);
+
+struct asObject *decoratorFileAsObj = asParseText(bigBedAutoSqlText(bbi));
+if (!asColumnNamesMatchFirstN(decoratorFileAsObj, decorationAsObj(), DECORATION_NUM_COLS))
+    {
+    errAbort ("Decoration file associated with track %s (%s) does not match the expected format - see decoration.as",
+            track->track, decoratorUrl);
+    return;
+    }
+
+struct trackDb *decoratorTdb = getTdbForDecorator(track->tdb);
+struct bigBedFilter *filters = bigBedBuildFilters(cart, bbi, decoratorTdb);
+struct lm *lm = lmInit(0);
+struct bigBedInterval *result = bigBedIntervalQuery(bbi, chromName, winStart, winEnd, 0, lm);
+
+struct mouseOverScheme *mouseScheme = mouseOverSetupForBbi(decoratorTdb, bbi);
+
+// insert resulting entries into track->decorators, leaving room for having multiple sources applied
+// to the same track in the future.
+if (track->decoratorGroup == NULL)
+    track->decoratorGroup = newDecoratorGroup();
+
+struct decorator* newDecorators = decoratorListFromBbi(decoratorTdb, chromName, result, filters, bbi->fieldCount, mouseScheme);
+track->decoratorGroup->decorators = slCat(track->decoratorGroup->decorators, newDecorators);
+for (struct decorator *d = track->decoratorGroup->decorators; d != NULL; d = d->next)
+    d->group = track->decoratorGroup;
+lmCleanup(&lm);
+bigBedFileClose(&bbi);
+}
+
 static void *remoteParallelLoad(void *threadParam)
 /* Each thread loads tracks in parallel until all work is done. */
 {
 pthread_t *pthread = threadParam;
 struct paraFetchData *pfd = NULL;
 pthread_detach(*pthread);  // this thread will never join back with it's progenitor
     // Canceled threads that might leave locks behind,
     // so the theads are detached and will be neither joined nor canceled.
 boolean allDone = FALSE;
 while(1)
     {
     pthread_mutex_lock( &pfdMutex );
     if (!pfdList)
 	{
 	allDone = TRUE;
@@ -7701,33 +7779,43 @@
 	return NULL;
 
     long thisTime = 0, lastTime = 0;
 
     if (measureTiming)
 	lastTime = clock1000();
 
     /* protect against errAbort */
     struct errCatch *errCatch = errCatchNew();
     if (errCatchStart(errCatch))
 	{
 	pfd->done = FALSE;
 	checkMaxWindowToDraw(pfd->track);
 	checkHideEmptySubtracks(pfd->track);
 	pfd->track->loadItems(pfd->track);
+        if (tdbHasDecorators(pfd->track))
+            {
+            loadDecorators(pfd->track);
+            decoratorMethods(pfd->track);
+            }
 	pfd->done = TRUE;
 	}
     errCatchEnd(errCatch);
+    if (errCatch->gotWarning)
+        {
+        // do something intelligent to permit reporting of warnings
+        // Can't pass it to warn yet - the fancy warnhandlers aren't ready
+        }
     if (errCatch->gotError)
 	{
 	pfd->track->networkErrMsg = cloneString(errCatch->message->string);
 	pfd->done = TRUE;
 	}
     errCatchFree(&errCatch);
 
     if (measureTiming)
 	{
 	thisTime = clock1000();
 	pfd->track->loadTime = thisTime - lastTime;
 	}
 
     pthread_mutex_lock( &pfdMutex );
     slRemoveEl(&pfdRunning, pfd);  // this list will not be huge
@@ -8358,30 +8446,31 @@
     hPrintf(" ");
     }
 hButtonMaybePressed("hgt.toggleRevCmplDisp", "reverse",
                        revCmplDisp ? "Show forward strand at this location - keyboard shortcut: r, then v"
                                    : "Show reverse strand at this location - keyboard shortcut: r, then v",
                        NULL, revCmplDisp);
 hPrintf(" ");
 
 hButtonWithOnClick("hgt.setWidth", "resize", "Resize image width to browser window size - keyboard shortcut: r, then s", "hgTracksSetWidth()");
 
 // put the track download interface behind hg.conf control
 if (cfgOptionBooleanDefault("showDownloadUi", FALSE))
     jsInline("var showDownloadButton = true;\n");
 }
 
+
 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 hideTracks = cgiOptionalString( "hideTracks") != NULL;
 boolean defaultTracks = cgiVarExists("hgt.reset");
 boolean showedRuler = FALSE;
 boolean showTrackControls = cartUsualBoolean(cart, "trackControlsOnMain", TRUE);
 boolean multiRegionButtonTop = cfgOptionBooleanDefault(MULTI_REGION_CFG_BUTTON_TOP, TRUE);
 long thisTime = 0, lastTime = 0;
@@ -8642,30 +8731,35 @@
 	    {
             if (!track->parallelLoading)
 		{
 		if (measureTiming)
 		    lastTime = clock1000();
 
 		checkMaxWindowToDraw(track);
 
 		checkHideEmptySubtracks(track);     // TODO: Test with multi-window feature
 
 		checkIfWiggling(cart, track);
 
 		if (!loadHack)
 		    {
 		    track->loadItems(track);
+                    if (tdbHasDecorators(track))
+                        {
+                        loadDecorators(track);
+                        decoratorMethods(track);
+                        }
 		    }
 		else
 		    {
 		    // TEMP HACK GALT REMOVE
 		    if (currentWindow == windows) // first window
 			{
 			track->loadItems(track);
 			}
 		    else
 			{
 			track->items = track->prevWindow->items;  // just point to the previous windows items (faster than loading)
 			// apparently loadItems is setting some other fields that we want, but which ones?
 			track->visibility = track->prevWindow->visibility;
 			track->limitedVis = track->prevWindow->limitedVis;
 			track->limitedVisSet = track->prevWindow->limitedVisSet;
@@ -8680,31 +8774,30 @@
 		    {
 		    thisTime = clock1000();
 		    track->loadTime = thisTime - lastTime;
 		    }
 		}
 	    }
 	}
 
     if (ptMax > 0)
 	{
 	/* wait for remote parallel load to finish */
 	remoteParallelLoadWait(atoi(cfgOptionDefault("parallelFetch.timeout", "90")));  // wait up to default 90 seconds.
 	if (measureTiming)
 	    measureTime("Waiting for parallel (%d threads for %d tracks) remote data fetch", ptMax, pfdListCount);
 	}
-
     }
 trackLoadingInProgress = FALSE;
 
 setGlobalsFromWindow(windows); // first window // restore globals
 trackList = windows->trackList;  // restore track list
 
 // Some loadItems() calls will have already set limitedVis.
 // Look for lowest limitedVis across all windows
 // if found, set all windows to same lowest limitedVis
 for (track = trackList; track != NULL; track = track->next)
     {
     setSharedLimitedVisAcrossWindows(track);
     struct track *sub;
     for (sub=track->subtracks; sub; sub=sub->next)
 	{