fecc0bfffd30dccc9998aa0c2e5a6064d487bbb1
chmalee
  Fri Apr 10 15:39:54 2020 -0700
Initial work on merging multiple bigBed items that span the window, refs #25133

diff --git src/hg/hgTracks/bigBedTrack.c src/hg/hgTracks/bigBedTrack.c
index 07606ff..8130474 100644
--- src/hg/hgTracks/bigBedTrack.c
+++ src/hg/hgTracks/bigBedTrack.c
@@ -379,30 +379,45 @@
 freeMem(rest);
 return field;
 }
 
 
 void bigBedAddLinkedFeaturesFromExt(struct track *track,
 	char *chrom, int start, int end, int scoreMin, int scoreMax, boolean useItemRgb,
 	int fieldCount, struct linkedFeatures **pLfList, int maxItems)
 /* Read in items in chrom:start-end from bigBed file named in track->bbiFileName, convert
  * them to linkedFeatures, and add to head of list. */
 {
 struct lm *lm = lmInit(0);
 struct trackDb *tdb = track->tdb;
 struct bigBedInterval *bb, *bbList = bigBedSelectRangeExt(track, chrom, start, end, lm, maxItems);
 char *mouseOverField = cartOrTdbString(cart, track->tdb, "mouseOverField", NULL);
+
+// check if this track can merge large items, this setting must be allowed in the trackDb
+// stanza for the track, but can be enabled/disabled via trackUi/right click menu so
+// we also need to check the cart for the current status
+int mergeCount = 0;
+boolean doWindowSizeFilter = trackDbSettingOn(track->tdb, MERGESPAN_TDB_SETTING);
+if (doWindowSizeFilter)
+    {
+    char hasMergedItemsSetting[256];
+    safef(hasMergedItemsSetting, sizeof(hasMergedItemsSetting), "%s.%s", track->track, MERGESPAN_CART_SETTING);
+    if (cartVarExists(cart, hasMergedItemsSetting))
+        doWindowSizeFilter = cartInt(cart, hasMergedItemsSetting) == 1;
+    else // save the cart var so javascript can offer the right toggle
+        cartSetInt(cart, hasMergedItemsSetting, 1);
+    }
 /* protect against temporary network error */
 struct bbiFile *bbi = NULL;
 struct errCatch *errCatch = errCatchNew();
 if (errCatchStart(errCatch))
     {
     bbi = fetchBbiForTrack(track);
     }
 errCatchEnd(errCatch);
 if (errCatch->gotError)
     {
     track->networkErrMsg = cloneString(errCatch->message->string);
     track->drawItems = bigDrawWarning;
     track->totalHeight = bigWarnTotalHeight;
     return;
     }
@@ -410,94 +425,157 @@
 
 int seqTypeField =  0;
 if (sameString(track->tdb->type, "bigPsl"))
     {
     seqTypeField =  bbExtraFieldIndex(bbi, "seqType");
     }
 
 int mouseOverIdx = bbExtraFieldIndex(bbi, mouseOverField);
 
 track->bbiFile = NULL;
 
 struct bigBedFilter *filters = bigBedBuildFilters(cart, bbi, track->tdb) ;
 if (compositeChildHideEmptySubtracks(cart, track->tdb, NULL, NULL))
    labelTrackAsHideEmpty(track);
 
+// a fake item that is the union of the items that span the current  window
+struct linkedFeatures *spannedLf = NULL;
 unsigned filtered = 0;
 for (bb = bbList; bb != NULL; bb = bb->next)
     {
     struct linkedFeatures *lf = NULL;
     if (sameString(track->tdb->type, "bigPsl"))
 	{
 	char *seq, *cds;
 	struct psl *psl = pslFromBigPsl(chromName, bb, seqTypeField,  &seq, &cds); 
 	int sizeMul =  pslIsProtein(psl) ? 3 : 1;
 	boolean isXeno = 0;  // just affects grayIx
 	boolean nameGetsPos = FALSE; // we want the name to stay the name
 
 	lf = lfFromPslx(psl, sizeMul, isXeno, nameGetsPos, track);
 	lf->original = psl;
 	if ((seq != NULL) && (lf->orientation == -1))
 	    reverseComplement(seq, strlen(seq));
 	lf->extra = seq;
 	lf->cds = cds;
 	}
     else if (sameString(tdb->type, "bigDbSnp"))
         {
         // bigDbSnp does not have a score field, but I want to compute the freqSourceIx from
         // trackDb and settings one time instead of for each item, so I'm overloading scoreMin.
         int freqSourceIx = scoreMin;
         lf = lfFromBigDbSnp(tdb, bb, filters, freqSourceIx);
         }
     else
 	{
         char startBuf[16], endBuf[16];
         char *bedRow[bbi->fieldCount];
-
         bigBedIntervalToRow(bb, chromName, startBuf, endBuf, bedRow, ArraySize(bedRow));
         if (bigBedFilterInterval(bedRow, filters))
             {
             struct bed *bed = bedLoadN(bedRow, fieldCount);
             lf = bedMungToLinkedFeatures(&bed, tdb, fieldCount,
                 scoreMin, scoreMax, useItemRgb);
             }
+        if (track->visibility != tvDense && lf && doWindowSizeFilter && bb->start < winStart && bb->end > winEnd)
+            {
+            mergeCount++;
+            struct bed *bed = bedLoadN(bedRow, fieldCount);
+            struct linkedFeatures *tmp = bedMungToLinkedFeatures(&bed, tdb, fieldCount,
+                scoreMin, scoreMax, useItemRgb);
+            if (spannedLf)
+                {
+                if (tmp->start < spannedLf->start)
+                    spannedLf->start = tmp->start;
+                if (tmp->end > spannedLf->end)
+                    spannedLf->end = tmp->end;
+                if (fieldCount > 9) // average the colors in the merged item
+                    {
+                    struct rgbColor itemColor = colorIxToRgb(lf->filterColor);
+                    struct rgbColor currColor = colorIxToRgb(spannedLf->filterColor);
+                    int r = currColor.r + round((itemColor.r - currColor.r) / mergeCount);
+                    int g = currColor.g + round((itemColor.g - currColor.g) / mergeCount);
+                    int b = currColor.b + round((itemColor.b - currColor.b) / mergeCount);
+                    spannedLf->filterColor = MAKECOLOR_32(r,g,b);
+                    }
+                }
+            else
+                {
+                // setting the label here protects against the case when only one item would
+                // have been merged. When this happens we warn in the track longLabel that
+                // nothing happened and essentially make the spanned item be what the actual
+                // item would be. If multiple items are merged then the labels and mouseOvers
+                // will get fixed up later
+                tmp->label = bigBedMakeLabel(track->tdb, track->labelColumns,  bb, chromName);
+                tmp->mouseOver   = restField(bb, mouseOverIdx);
+                slAddHead(&spannedLf, tmp);
+                }
+            continue; // lf will be NULL, but these items aren't "filtered", they're merged
+            }
 	}
 
     if (lf == NULL)
         {
         filtered++;
         continue;
         }
 
     if (lf->label == NULL)
         lf->label = bigBedMakeLabel(track->tdb, track->labelColumns,  bb, chromName);
     if (sameString(track->tdb->type, "bigGenePred") || startsWith("genePred", track->tdb->type))
         {
         lf->original = genePredFromBigGenePred(chromName, bb); 
         }
 
     if (lf->mouseOver == NULL)
         {
         char* mouseOver = restField(bb, mouseOverIdx);
         lf->mouseOver   = mouseOver; // leaks some memory, cloneString handles NULL ifself 
         }
     slAddHead(pLfList, lf);
     }
 
 if (filtered)
    labelTrackAsFilteredNumber(track, filtered);
 
+if (doWindowSizeFilter)
+    // add the number of merged items to the track longLabel
+    {
+    char labelBuf[256];
+    if (mergeCount > 1)
+        safef(labelBuf, sizeof(labelBuf), " (Merged %d items)", mergeCount);
+    else
+        safef(labelBuf, sizeof(labelBuf), " (No Items Merged in window)");
+    track->longLabel = catTwoStrings(track->longLabel, labelBuf);
+    }
+
+if (spannedLf)
+    {
+    // if two or more items were merged together, fix up the label of the special merged item,
+    // otherwise the label and mouseOver will be the normal bed one
+    char itemLabelBuf[256], mouseOver[256];
+    if (mergeCount > 1)
+        {
+        safef(itemLabelBuf, sizeof(itemLabelBuf), "Merged %d items", mergeCount);
+        safef(mouseOver, sizeof(mouseOver), "Merged %d items. Right-click and select 'show merged items' to expand", mergeCount);
+        spannedLf->label = cloneString(itemLabelBuf);
+        spannedLf->name = "mergedItem"; // always, for correct hgc clicks
+        spannedLf->mouseOver = cloneString(mouseOver);
+        }
+    slAddHead(pLfList, spannedLf);
+    }
+
 lmCleanup(&lm);
 
 if (!trackDbSettingClosestToHomeOn(track->tdb, "linkIdInName"))
     track->itemName = bigLfItemName;
 bbiFileClose(&bbi);
 }
 
 
 boolean canDrawBigBedDense(struct track *tg)
 /* Return TRUE if conditions are such that can do the fast bigBed dense data fetch and
  * draw. */
 {
 return tg->isBigBed;
 }