f5ba7e5371b8bcf6c260d065b971e57c09eca1ad
Merge parents 189bd07 d24ed48
kate
  Wed Sep 25 10:56:06 2019 -0700
Merging

diff --cc src/hg/hgTracks/hgTracks.c
index 6d319cf,f5125d5..e883494
--- src/hg/hgTracks/hgTracks.c
+++ src/hg/hgTracks/hgTracks.c
@@@ -1,10426 -1,10270 +1,10402 @@@
  /* hgTracks - the /riginal, and still the largest module for the UCSC Human Genome
   * Browser main cgi script.  Currently contains most of the track framework, though
   * there's quite a bit of other framework type code in simpleTracks.c.  The main
   * routine got moved to create a new entry point to the bulk of the code for the
   * hgRenderTracks web service.  See mainMain.c for the main used by the hgTracks CGI. */
  
  /* Copyright (C) 2014 The Regents of the University of California 
   * See README in this or parent directory for licensing information. */
  
  #include <pthread.h>
  #include <limits.h>
  #include "common.h"
  #include "hCommon.h"
  #include "linefile.h"
  #include "portable.h"
  #include "memalloc.h"
  #include "localmem.h"
  #include "obscure.h"
  #include "dystring.h"
  #include "hash.h"
  #include "jksql.h"
  #include "gfxPoly.h"
  #include "memgfx.h"
  #include "hvGfx.h"
  #include "psGfx.h"
  #include "cheapcgi.h"
  #include "hPrint.h"
  #include "htmshell.h"
  #include "cart.h"
  #include "hdb.h"
  #include "hui.h"
  #include "hgFind.h"
  #include "hgTracks.h"
  #include "trashDir.h"
  #include "grp.h"
  #include "versionInfo.h"
  #include "web.h"
  #include "cds.h"
  #include "cutterTrack.h"
  #include "wikiTrack.h"
  #include "ctgPos.h"
  #include "bed.h"
  #include "bigBed.h"
  #include "bigWig.h"
  #include "bedCart.h"
  #include "udc.h"
  #include "customTrack.h"
  #include "trackHub.h"
  #include "hubConnect.h"
  #include "cytoBand.h"
  #include "ensFace.h"
  #include "pcrResult.h"
  #include "jsHelper.h"
  #include "mafTrack.h"
  #include "hgConfig.h"
  #include "encode.h"
  #include "agpFrag.h"
  #include "imageV2.h"
  #include "suggest.h"
  #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"
 +#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",
  	    "hgt.collapseGroups", "hgt.expandGroups", "hgt.suggest",
  	    "hgt.jump", "hgt.refresh", "hgt.setWidth",
              "hgt.trackImgOnly", "hgt.ideogramToo", "hgt.trackNameFilter", "hgt.imageV1", "hgt.suggestTrack", "hgt.setWidth",
               TRACK_SEARCH,         TRACK_SEARCH_ADD_ROW,     TRACK_SEARCH_DEL_ROW, TRACK_SEARCH_PAGER,
              "hgt.contentType", "hgt.positionInput", "hgt.internal",
              "sortExp", "sortSim", "hideTracks", "ignoreCookie",
              NULL };
  
  /* These variables persist from one incarnation of this program to the
   * next - living mostly in the cart. */
  boolean baseShowPos;           /* TRUE if should display full position at top of base track */
  boolean baseShowAsm;           /* TRUE if should display assembly info at top of base track */
  boolean baseShowScaleBar;      /* TRUE if should display scale bar at very top of base track */
  boolean baseShowRuler;         /* TRUE if should display the basic ruler in the base track (default) */
  char *baseTitle = NULL;        /* Title it should display top of base track (optional)*/
  static char *userSeqString = NULL;  /* User sequence .fa/.psl file. */
  
  /* These variables are set by getPositionFromCustomTracks() at the very
   * beginning of tracksDisplay(), and then used by loadCustomTracks(). */
  char *ctFileName = NULL;    /* Custom track file. */
  struct customTrack *ctList = NULL;  /* Custom tracks. */
  boolean hasCustomTracks = FALSE;  /* whether any custom tracks are for this db*/
  struct slName *browserLines = NULL; /* Custom track "browser" lines. */
  
  boolean withNextItemArrows = FALSE; /* Display next feature (gene) navigation buttons */
  boolean withPriorityOverride = FALSE;   /* Display priority for each track to allow reordering */
  
  int gfxBorder = hgDefaultGfxBorder; /* Width of graphics border. */
  int guidelineSpacing = 12;  /* Pixels between guidelines. */
  
  boolean withIdeogram = TRUE;            /* Display chromosome ideogram? */
  
  int rulerMode = tvHide;         /* on, off, full */
  struct hvGfx *hvgSide = NULL;   // Extra pointer to a sideLabel image that can be built if needed
  
  char *rulerMenu[] =
  /* dropdown for ruler visibility */
      {
      "hide",
      "dense",
      "full"
      };
  
  char *protDbName;               /* Name of proteome database for this genome. */
  #define MAX_CONTROL_COLUMNS 6
  #define LOW 1
  #define MEDIUM 2
  #define BRIGHT 3
  #define MAXCHAINS 50000000
  boolean hgDebug = FALSE;      /* Activate debugging code. Set to true by hgDebug=on in command line*/
  int imagePixelHeight = 0;
  struct hash *oldVars = NULL;
  struct jsonElement *jsonForClient = NULL;
  
  boolean hideControls = FALSE;           /* Hide all controls? */
  boolean trackImgOnly = FALSE;           /* caller wants just the track image and track table html */
  boolean ideogramToo =  FALSE;           /* caller wants the ideoGram (when requesting just one track) */
  
  /* Structure returned from resolvePosition.
   * We use this to to expand any tracks to full
   * that were found to contain the searched-upon
   * position string */
  struct hgPositions *hgp = NULL;
  
  /* Other global variables. */
  struct group *groupList = NULL;    /* List of all tracks. */
  char *browserName;              /* Test, preview, or public browser */
  char *organization;             /* UCSC */
  
  struct hash *trackHash = NULL; /* Hash of the tracks by their name. */
  
  #ifdef DEBUG
  void uglySnoopTrackList(int depth, struct track *trackList)
  /* Print out some info on track list. */
  {
  struct track *track;
  for (track = trackList; track != NULL; track = track->next)
      {
      if (stringIn("FaireH1h", track->track))
  	{
  	repeatCharOut(uglyOut, '+', depth);
  	}
      uglySnoopTrackList(depth+1, track->subtracks);
      }
  }
  #endif /* DEBUG */
  
  struct track *trackFindByName(struct track *tracks, char *trackName)
  /* find a track in tracks by name, recursively searching subtracks */
  {
  struct track *track;
  for (track = tracks; track != NULL; track = track->next)
      {
      if (sameString(track->track, trackName))
          return track;
      else if (track->subtracks != NULL)
          {
          struct track *st = trackFindByName(track->subtracks, trackName);
          if (st != NULL)
              return st;
          }
      }
  return NULL;
  }
  
  int tgCmpPriority(const void *va, const void *vb)
  /* Compare to sort based on priority; use shortLabel as secondary sort key. */
  {
  const struct track *a = *((struct track **)va);
  const struct track *b = *((struct track **)vb);
  float dif = 0;
  if (a->group && b->group)
      dif = a->group->priority - b->group->priority;
  if (dif == 0)
      dif = a->priority - b->priority;
  if (dif < 0)
     return -1;
  else if (dif == 0.0)
      /* secondary sort on label */
      return strcasecmp(a->shortLabel, b->shortLabel);
  else
     return 1;
  }
  
  int trackRefCmpPriority(const void *va, const void *vb)
  /* Compare based on priority. */
  {
  const struct trackRef *a = *((struct trackRef **)va);
  const struct trackRef *b = *((struct trackRef **)vb);
  return tgCmpPriority(&a->track, &b->track);
  }
  
  int gCmpPriority(const void *va, const void *vb)
  /* Compare groups based on priority. */
  {
  const struct group *a = *((struct group **)va);
  const struct group *b = *((struct group **)vb);
  float dif = a->priority - b->priority;
  
  if (dif == 0)
      return 0;
  if (dif < 0)
     return -1;
  else if (dif == 0.0)
     return 0;
  else
     return 1;
  }
  
  void changeTrackVis(struct group *groupList, char *groupTarget, int changeVis)
  /* Change track visibilities. If groupTarget is
   * NULL then set visibility for tracks in all groups.  Otherwise,
   * just set it for the given group.  If vis is -2, then visibility is
   * unchanged.  If -1 then set visibility to default, otherwise it should
   * be tvHide, tvDense, etc.
   * If we are going back to default visibility, then reset the track
   * ordering also. */
  {
  struct group *group;
  if (changeVis == -2)
      return;
  for (group = groupList; group != NULL; group = group->next)
      {
      struct trackRef *tr;
      if (groupTarget == NULL || sameString(group->name,groupTarget))
          {
          static char pname[512];
          /* if default vis then reset group priority */
          if (changeVis == -1)
              group->priority = group->defaultPriority;
          for (tr = group->trackList; tr != NULL; tr = tr->next)
              {
              struct track *track = tr->track;
              struct trackDb *tdb = track->tdb;
              if (changeVis == -1) // to default
                  {
                  if (tdbIsComposite(tdb))
                      {
                      safef(pname, sizeof(pname),"%s.*",track->track); //to remove all settings associated with this composite!
                      cartRemoveLike(cart,pname);
                      struct track *subTrack;
                      for (subTrack = track->subtracks;subTrack != NULL; subTrack = subTrack->next)
                          {
                          subTrack->visibility = tdb->visibility;
                          cartRemove(cart, subTrack->track);
                          }
                      }
  
                  /* restore defaults */
                  if (tdbIsSuperTrackChild(tdb) || tdbIsCompositeChild(tdb))
                      {
                      assert(tdb->parent != NULL && tdb->parent->track);
                      cartRemove(cart, tdb->parent->track);
                      if (withPriorityOverride)
                          {
                          safef(pname, sizeof(pname), "%s.priority",tdb->parent->track);
                          cartRemove(cart, pname);
                          }
                      }
  
                  track->visibility = tdb->visibility;
                  cartRemove(cart, track->track);
  
                  /* set the track priority back to the default value */
                  if (withPriorityOverride)
                      {
                      safef(pname, sizeof(pname), "%s.priority",track->track);
                      cartRemove(cart, pname);
                      track->priority = track->defaultPriority;
                      }
                  }
              else // to changeVis value (Usually tvHide)
                  {
                  /* change to specified visibility */
                  if (tdbIsSuperTrackChild(tdb))
                      {
                      assert(tdb->parent != NULL);
                      /* Leave supertrack members alone -- only change parent */
                      struct trackDb *parentTdb = tdb->parent;
                      if ((changeVis == tvHide && !parentTdb->isShow) ||
                          (changeVis != tvHide && parentTdb->isShow))
                          {
                          /* remove if setting to default vis */
                          cartRemove(cart, parentTdb->track);
                          }
                      else
                          cartSetString(cart, parentTdb->track,
                                      changeVis == tvHide ? "hide" : "show");
                      }
                  else // Not super  child
                      {
                      if (changeVis == tdb->visibility)
                          /* remove if setting to default vis */
                          cartRemove(cart, track->track);
                      else
                          cartSetString(cart, track->track, hStringFromTv(changeVis));
                      track->visibility = changeVis;
                      }
  
                  // Whether super child or not, if its a composite, then handle the children
                  if (tdbIsComposite(tdb))
                      {
                      struct track *subtrack;
                      for (subtrack=track->subtracks;subtrack!=NULL;subtrack=subtrack->next)
                          {
                          if (changeVis == tvHide)               // Since subtrack level vis is an
                              cartRemove(cart, subtrack->track); // override, simply remove to hide
                          else
                              cartSetString(cart, subtrack->track, hStringFromTv(changeVis));
                          subtrack->visibility = changeVis;
                          }
                      }
                  }
              }
          }
      }
  slSort(&groupList, gCmpPriority);
  }
  
  int trackOffsetX()
  /* Return x offset where track display proper begins. */
  {
  int x = gfxBorder;
  if (withLeftLabels)
      x += tl.leftLabelWidth + gfxBorder;
  return x;
  }
  
  
  static void mapBoxTrackUi(struct hvGfx *hvg, int x, int y, int width,
                            int height, char *name, char *shortLabel, char *id)
  /* Print out image map rectangle that invokes hgTrackUi. */
  {
  x = hvGfxAdjXW(hvg, x, width);
  char *url = trackUrl(name, chromName);
  
  if (theImgBox && curImgTrack)
      {
      struct imgSlice *curSlice = imgTrackSliceGetByType(curImgTrack,stButton);
      if (curSlice)
          sliceAddLink(curSlice,url,shortLabel);
      }
  else
      {
      hPrintf("<AREA SHAPE=RECT COORDS=\"%d,%d,%d,%d\" ", x, y, x+width, y+height);
      hPrintf("HREF=\"%s\"", url);
      mapStatusMessage("%s controls", shortLabel);
      hPrintf(">\n");
      }
  freeMem(url);
  }
  
  static void mapBoxToggleComplement(struct hvGfx *hvg, int x, int y, int width, int height,
                                     struct track *toggleGroup, char *chrom,
                                     int start, int end, char *message)
  /*print out a box along the DNA bases that toggles a cart variable
   * "complement" to complement the DNA bases at the top by the ruler*/
  {
  struct dyString *ui = uiStateUrlPart(toggleGroup);
  x = hvGfxAdjXW(hvg, x, width);
  if (theImgBox && curImgTrack)
      {
      char link[512];
      safef(link,sizeof(link),"%s?complement_%s=%d&%s", hgTracksName(), database,
            !cartUsualBooleanDb(cart, database, COMPLEMENT_BASES_VAR, FALSE),ui->string);
      imgTrackAddMapItem(curImgTrack,link,(char *)(message != NULL?message:NULL),x, y, x+width, y+height, NULL);
      }
  else
      {
      hPrintf("<AREA SHAPE=RECT COORDS=\"%d,%d,%d,%d\" ", x, y, x+width, y+height);
      hPrintf("HREF=\"%s?complement_%s=%d", hgTracksName(), database,
              !cartUsualBooleanDb(cart, database, COMPLEMENT_BASES_VAR, FALSE));
      hPrintf("&%s\"", ui->string);
      freeDyString(&ui);
      if (message != NULL)
          mapStatusMessage("%s", message);
      hPrintf(">\n");
      }
  }
  
  char *trackUrl(char *mapName, char *chromName)
  /* Return hgTrackUi url; chromName is optional. */
  {
  char *encodedMapName = cgiEncode(mapName);
  char buf[2048];
  char *hgTrackUi = hTrackUiForTrack(mapName);
  if(chromName == NULL)
      safef(buf, sizeof(buf), "%s?%s=%s&g=%s", hgTrackUi, cartSessionVarName(), cartSessionId(cart), encodedMapName);
  else
      safef(buf, sizeof(buf), "%s?%s=%s&c=%s&g=%s", hgTrackUi, cartSessionVarName(), cartSessionId(cart), chromName, encodedMapName);
  freeMem(encodedMapName);
  return(cloneString(buf));
  }
  
- #ifdef REMOTE_TRACK_AJAX_CALLBACK
- static boolean trackUsesRemoteData(struct track *track)
- /* returns TRUE is this track has a remote datasource */
- {
- if (!IS_KNOWN(track->remoteDataSource))
-     {
-     SET_TO_NO(track->remoteDataSource);
-     //if (track->bbiFile != NULL)   // FIXME: Chicken or the egg. bigWig/bigBed "bbiFile" filled
-     //                              //        in by loadItems, but we don't want to load items.
-     //    {
-     //    if (!startsWith("/gbdb/",track->bbiFile->fileName))
-     //        SET_TO_YES(track->remoteDataSource);
-     //    }
-     if (startsWithWord("bigWig",track->tdb->type) || startsWithWord("bigBed",track->tdb->type) ||
- 	startsWithWord("halSnake",track->tdb->type) ||
- 	startsWithWord("pslSnake",track->tdb->type) ||
- 	startsWithWord("bigPsl",track->tdb->type) ||
- 	startsWithWord("bigGenePred",track->tdb->type) ||
- 	startsWithWord("bigChain",track->tdb->type) ||
- 	startsWithWord("bigMaf",track->tdb->type) ||
- 	startsWithWord("bam",track->tdb->type) || startsWithWord("vcfTabix", track->tdb->type))
-         {
-         SET_TO_YES(track->remoteDataSource);
-         }
-     }
- return IS_YES(track->remoteDataSource);
- }
- 
- boolean trackShouldUseAjaxRetrieval(struct track *track)
- /* Tracks with remote data sources should berendered via an ajax callback */
- {
- return (theImgBox && !trackImgOnly && trackUsesRemoteData(track));
- }
- #endif///def REMOTE_TRACK_AJAX_CALLBACK
- 
  static boolean isCompositeInAggregate(struct track *track)
  // Check to see if this is a custom composite in aggregate mode.
  {
  if (!isCustomComposite(track->tdb))
      return FALSE;
  
  char *aggregateVal = cartOrTdbString(cart, track->tdb, "aggregate", NULL);
  if ((aggregateVal == NULL) || sameString(aggregateVal, "none"))
      return FALSE;
  
  struct track *subtrack = NULL;
  for (subtrack = track->subtracks; subtrack != NULL; subtrack = subtrack->next)
      {
      if (isSubtrackVisible(subtrack))
          break;
      }
  if (subtrack == NULL)
      return FALSE;
  
  for (subtrack = track->subtracks; subtrack != NULL; subtrack = subtrack->next)
      {
      subtrack->mapsSelf = FALSE;	/* Round about way to tell wig not to do own mapping. */
      }
  
  multiWigContainerMethods(track);
  //struct wigCartOptions *wigCart = wigCartOptionsNew(cart, track->tdb, 0, NULL);
  //track->wigCartData = (void *) wigCart;
  //track->lineHeight = wigCart->defaultHeight;
  //wigCart->isMultiWig = TRUE;
  //wigCart->autoScale = wiggleScaleAuto;
  //wigCart->defaultHeight = track->lineHeight;
  //struct wigGraphOutput *wgo = setUpWgo(xOff, yOff, width, tg->height, numTracks, wigCart, hvg);
  //tg->wigGraphOutput = wgo;
  
  return TRUE;
  }
  
  static int trackPlusLabelHeight(struct track *track, int fontHeight)
  /* Return the sum of heights of items in this track (or subtrack as it may be)
   * and the center label(s) above the items (if any). */
  {
- if (trackShouldUseAjaxRetrieval(track))
-     return REMOTE_TRACK_HEIGHT;
- 
  enum trackVisibility vis = limitVisibility(track);
  int y = track->totalHeight(track, vis);
  if (isCenterLabelIncluded(track))
      y += fontHeight;
  if (tdbIsComposite(track->tdb) && !isCompositeInAggregate(track))
      {
      struct track *subtrack;
      for (subtrack = track->subtracks;  subtrack != NULL; subtrack = subtrack->next)
          {
          if (isSubtrackVisible(subtrack) &&  isCenterLabelIncluded(subtrack))
              y += fontHeight;
          }
      }
  return y;
  }
  
  void drawColoredButtonBox(struct hvGfx *hvg, int x, int y, int w, int h,
                                  int enabled, Color shades[])
  /* draw button box, providing shades of the desired button color */
  {
  int light = shades[1], mid = shades[2], dark = shades[4];
  if (enabled)
      {
      hvGfxBox(hvg, x, y, w, 1, light);
      hvGfxBox(hvg, x, y+1, 1, h-1, light);
      hvGfxBox(hvg, x+1, y+1, w-2, h-2, mid);
      hvGfxBox(hvg, x+1, y+h-1, w-1, 1, dark);
      hvGfxBox(hvg, x+w-1, y+1, 1, h-1, dark);
      }
  else                /* try to make the button look as if
                   * it is already depressed */
      {
      hvGfxBox(hvg, x, y, w, 1, dark);
      hvGfxBox(hvg, x, y+1, 1, h-1, dark);
      hvGfxBox(hvg, x+1, y+1, w-2, h-2, light);
      hvGfxBox(hvg, x+1, y+h-1, w-1, 1, light);
      hvGfxBox(hvg, x+w-1, y+1, 1, h-1, light);
      }
  }
  
  void drawGrayButtonBox(struct hvGfx *hvg, int x, int y, int w, int h, int enabled)
  /* Draw a gray min-raised looking button. */
  {
      drawColoredButtonBox(hvg, x, y, w, h, enabled, shadesOfGray);
  }
  
  void drawBlueButtonBox(struct hvGfx *hvg, int x, int y, int w, int h, int enabled)
  /* Draw a blue min-raised looking button. */
  {
      drawColoredButtonBox(hvg, x, y, w, h, enabled, shadesOfSea);
  }
  
  void drawButtonBox(struct hvGfx *hvg, int x, int y, int w, int h, int enabled)
  /* Draw a standard (gray) min-raised looking button. */
  {
      drawGrayButtonBox(hvg, x, y, w, h, enabled);
  }
  
  void beforeFirstPeriod( char *str )
  {
  char *t = rindex( str, '.' );
  
  if( t == NULL )
      return;
  else
      str[strlen(str) - strlen(t)] = '\0';
  }
  
  static void drawBases(struct hvGfx *hvg, int x, int y, int width, int height,
                          Color color, MgFont *font, boolean complementSeq,
                          struct dnaSeq *thisSeq)
  /* Draw evenly spaced bases. */
  {
  struct dnaSeq *seq;
  
  if (thisSeq == NULL)
      seq = hDnaFromSeq(database, chromName, winStart, winEnd, dnaUpper);
  else
      seq = thisSeq;
  
  if (complementSeq)
      complement(seq->dna, seq->size);
  spreadBasesString(hvg, x, y, width, height, color, font,
                                  seq->dna, seq->size, FALSE);
  
  if (thisSeq == NULL)
      freeDnaSeq(&seq);
  }
  
  void drawComplementArrow( struct hvGfx *hvg, int x, int y,
                                  int width, int height, MgFont *font)
  /* Draw arrow and create clickbox for complementing ruler bases */
  {
  boolean baseCmpl = cartUsualBooleanDb(cart, database, COMPLEMENT_BASES_VAR, FALSE);
  // reverse arrow when base complement doesn't match display
  char *text =  (baseCmpl == revCmplDisp) ? "--->" : "<---";
  hvGfxTextRight(hvg, x, y, width, height, MG_BLACK, font, text);
  mapBoxToggleComplement(hvg, x, y, width, height, NULL, chromName, winStart, winEnd,
                         "complement bases");
  }
  
  struct track *chromIdeoTrack(struct track *trackList)
  /* Find chromosome ideogram track */
  {
  struct track *track;
  for(track = trackList; track != NULL; track = track->next)
      {
      if(sameString(trackHubSkipHubName(track->track), "cytoBandIdeo"))
  	{
  	if (hTableExists(database, track->table))
  	    return track;
  	else
  	    return NULL;
  	}
      }
  return NULL;
  }
  
  void removeTrackFromGroup(struct track *track)
  /* Remove track from group it is part of. */
  {
  struct trackRef *tr = NULL;
  for(tr = track->group->trackList; tr != NULL; tr = tr->next)
      {
      if(tr->track == track)
  	{
  	slRemoveEl(&track->group->trackList, tr);
  	break;
  	}
      }
  }
  
  
  void fillInStartEndBands(struct track *ideoTrack, char *startBand, char *endBand, int buffSize)
  /* Loop through the bands and fill in the one that the current window starts
     on and ends on. */
  {
  struct cytoBand *cb = NULL, *cbList = ideoTrack->items;
  for(cb = cbList; cb != NULL; cb = cb->next)
      {
      /* If the start or end is encompassed by this band fill
         it in. */
      if (winStart >= cb->chromStart
      &&  winStart <= cb->chromEnd)
          {
          safef(startBand, buffSize, "%s", cb->name);
          }
      /* End is > rather than >= due to odditiy in the
         cytoband track where the starts and ends of two
         bands overlaps by one. */
      if (winEnd >  cb->chromStart
      &&  winEnd <= cb->chromEnd)
          {
          safef(endBand, buffSize, "%s", cb->name);
          }
      }
  }
  
  boolean makeChromIdeoImage(struct track **pTrackList, char *psOutput,
                          struct tempName *ideoTn)
  /* Make an ideogram image of the chromosome and our position in it.  If the
   * ideoTn parameter is not NULL, it is filled in if the ideogram is created. */
  {
  struct track *ideoTrack = NULL;
  MgFont *font = tl.font;
  char *mapName = "ideoMap";
  struct hvGfx *hvg;
  boolean doIdeo = TRUE;
  int ideoWidth = round(.8 *tl.picWidth);
  int ideoHeight = 0;
  int textWidth = 0;
  struct tempName pngTn;
  boolean nukeIdeoFromList = FALSE;
  if (ideoTn == NULL)
      ideoTn = &pngTn;   // not returning value
  
  ideoTrack = chromIdeoTrack(*pTrackList);
  
  /* If no ideogram don't draw. */
  if(ideoTrack == NULL)
      {
      doIdeo = FALSE;
      }
  else if(trackImgOnly && !ideogramToo)
      {
      doIdeo = FALSE;
      }
  else
      {
      /* Remove the track from the group and track list. */
      removeTrackFromGroup(ideoTrack);
      slRemoveEl(pTrackList, ideoTrack);
      nukeIdeoFromList = TRUE;
  
      /* Fix for hide all button hiding the ideogram as well. */
      if(withIdeogram && ideoTrack->items == NULL)
  	{
  	ideoTrack->visibility = tvDense;
  	ideoTrack->loadItems(ideoTrack);
  	}
      limitVisibility(ideoTrack);
  
      /* If hidden don't draw. */
      if(ideoTrack->limitedVis == tvHide || !withIdeogram)
          doIdeo = FALSE;
      }
  // TODO use DIV in future (can update entire div at once in hgTracks.js)
  //hPrintf("<DIV id='chromIdeoDiv'>\n");
  // FYI from testing, I see that there is code that inserts warning error messages
  //  right before ideoMap, so any changes to that name or adding the DIV would require
  //  updating the warning-insertion target name.
  if(doIdeo)
      {
      char startBand[1024];
      char endBand[1024];
      char title[1024];
      startBand[0] = endBand[0] = '\0';
      fillInStartEndBands(ideoTrack, startBand, endBand, sizeof(startBand));
      /* Start up client side map. */
      if (!psOutput)
          hPrintf("<MAP Name=%s>\n", mapName);
      /* Draw the ideogram. */
      ideoHeight = gfxBorder + ideoTrack->height;
      if (psOutput)
          {
          trashDirFile(ideoTn, "hgtIdeo", "hgtIdeo", ".ps");
          hvg = hvGfxOpenPostScript(ideoWidth, ideoHeight, ideoTn->forCgi);
          }
      else
          {
          trashDirFile(ideoTn, "hgtIdeo", "hgtIdeo", ".png");
          hvg = hvGfxOpenPng(ideoWidth, ideoHeight, ideoTn->forCgi, FALSE);
          }
      hvg->rc = revCmplDisp;
      initColors(hvg);
      ideoTrack->ixColor = hvGfxFindRgb(hvg, &ideoTrack->color);
      ideoTrack->ixAltColor = hvGfxFindRgb(hvg, &ideoTrack->altColor);
      hvGfxSetClip(hvg, 0, gfxBorder, ideoWidth, ideoTrack->height);
      if (virtMode)
  	{
      	if (!sameString(virtModeShortDescr,""))
      	    safef(title, sizeof(title), "%s (%s)", chromName, virtModeShortDescr);
  	else
      	    safef(title, sizeof(title), "%s (%s)", chromName, virtModeType);
  	}
      else if (isEmpty(startBand))
          safef(title, sizeof(title), "%s", chromName);
      else if (sameString(startBand, endBand))
          safef(title, sizeof(title), "%s (%s)", chromName, startBand);
      else
          safef(title, sizeof(title), "%s (%s-%s)", chromName, startBand, endBand);
      textWidth = mgFontStringWidth(font, title);
      hvGfxTextCentered(hvg, 2, gfxBorder, textWidth, ideoTrack->height, MG_BLACK, font, title);
      // cytoBandDrawAt() clips x based on insideX+insideWidth,
      // but in virtMode we may be in a window that is smaller than the ideo width
      // so temporarily set them to the actual ideo graphic offset and width
      int saveInsideX = insideX;
      int saveInsideWidth = insideWidth;
      insideX = textWidth+4;
      insideWidth = ideoWidth-insideX;
      ideoTrack->drawItems(ideoTrack, winStart, winEnd, hvg, insideX, gfxBorder,
                           insideWidth, font, ideoTrack->ixColor, ideoTrack->limitedVis);
      insideX = saveInsideX;
      insideWidth = saveInsideWidth;
      hvGfxUnclip(hvg);
      /* Save out picture and tell html file about it. */
      hvGfxClose(&hvg);
      /* Finish map. */
      if (!psOutput)
  	{
          hPrintf("</MAP>\n");
  	jsInline("$('area.cytoBand').click(function(){return false;});\n");
  	}
      }
  
  // create an empty hidden-map place holder which can change dynamically with ajax callbacks.
  if (!doIdeo && !psOutput)
      {
      hPrintf("<MAP Name=%s>\n", mapName);
      hPrintf("</MAP>\n");
      }
  
  hPrintf("<TABLE BORDER=0 CELLPADDING=0>");
  if (!psOutput)
      {
      // by default, create an empty hidden ideo place holder for future dynamic ajax update
      char *srcPath = "";
      char *style = "display: none;";
      if (doIdeo)
  	{
  	srcPath = ideoTn->forHtml;
  	style = "display: inline;";
  	}
      hPrintf("<TR><TD HEIGHT=5></TD></TR>");
      hPrintf("<TR><TD><IMG SRC = \"%s\" BORDER=1 WIDTH=%d HEIGHT=%d USEMAP=#%s id='chrom' style='%s'>",
              srcPath, ideoWidth, ideoHeight, mapName, style);
      hPrintf("</TD></TR>");
      hPrintf("<TR><TD HEIGHT=5></TD></TR></TABLE>\n");
      }
  else
      hPrintf("<TR><TD HEIGHT=10></TD></TR></TABLE>\n");
  //hPrintf("</DIV>\n");  // TODO use DIV in future
  if(ideoTrack != NULL)
      {
      ideoTrack->limitedVisSet = TRUE;
      ideoTrack->limitedVis = tvHide; /* Don't draw in main gif. */
      }
  return nukeIdeoFromList;
  }
  
  char *pcrResultMapItemName(struct track *tg, void *item)
  /* Stitch accession and display name back together (if necessary). */
  {
  struct linkedFeatures *lf = item;
  return pcrResultItemAccName(lf->name, lf->extra);
  }
  
  void pcrResultLoad(struct track *tg)
  /* Load locations of primer matches into linkedFeatures items. */
  {
  char *pslFileName, *primerFileName;
  struct targetDb *target;
  if (! pcrResultParseCart(database, cart, &pslFileName, &primerFileName, &target))
      return;
  
  /* Don't free psl -- used in drawing phase by baseColor code. */
  struct psl *pslList = pslLoadAll(pslFileName), *psl;
  struct linkedFeatures *itemList = NULL;
  if (target != NULL)
      {
      int rowOffset = hOffsetPastBin(database, chromName, target->pslTable);
      struct sqlConnection *conn = hAllocConn(database);
      struct sqlResult *sr;
      char **row;
      char query[2048];
      struct psl *tpsl;
      for (tpsl = pslList;  tpsl != NULL;  tpsl = tpsl->next)
  	{
  	char *itemAcc = pcrResultItemAccession(tpsl->tName);
  	char *itemName = pcrResultItemName(tpsl->tName);
  	/* Query target->pslTable to get target-to-genomic mapping: */
  	sqlSafef(query, sizeof(query), "select * from %s where qName = '%s'",
  	      target->pslTable, itemAcc);
  	sr = sqlGetResult(conn, query);
  	while ((row = sqlNextRow(sr)) != NULL)
  	    {
  	    struct psl *gpsl = pslLoad(row+rowOffset);
  	    if (sameString(gpsl->tName, chromName) && gpsl->tStart < winEnd && gpsl->tEnd > winStart)
  		{
  		struct psl *trimmed = pslTrimToQueryRange(gpsl, tpsl->tStart,
  				      tpsl->tEnd);
  		struct linkedFeatures *lf;
  		char *targetStyle = cartUsualString(cart,
  		     PCR_RESULT_TARGET_STYLE, PCR_RESULT_TARGET_STYLE_DEFAULT);
  		if (sameString(targetStyle, PCR_RESULT_TARGET_STYLE_TALL))
  		    {
  		    lf = lfFromPslx(gpsl, 1, FALSE, FALSE, tg);
  		    lf->tallStart = trimmed->tStart;
  		    lf->tallEnd = trimmed->tEnd;
  		    }
  		else
  		    {
  		    lf = lfFromPslx(trimmed, 1, FALSE, FALSE, tg);
  		    }
  		lf->name = cloneString(itemAcc);
  		char extraInfo[512];
  		safef(extraInfo, sizeof(extraInfo), "%s|%d|%d",
  		      (itemName ? itemName : ""), tpsl->tStart, tpsl->tEnd);
  		lf->extra = cloneString(extraInfo);
  		slAddHead(&itemList, lf);
  		}
  	    }
  	}
      hFreeConn(&conn);
      }
  else
      for (psl = pslList;  psl != NULL;  psl = psl->next)
      if (sameString(psl->tName, chromName) && psl->tStart < winEnd && psl->tEnd > winStart)
              {
              struct linkedFeatures *lf = lfFromPslx(psl, 1, FALSE, FALSE, tg);
              lf->name = cloneString("");
              lf->extra = cloneString("");
              slAddHead(&itemList, lf);
              }
  slSort(&itemList, linkedFeaturesCmp);
  tg->items = itemList;
  }
  
  char *pcrResultTrackItemName(struct track *tg, void *item)
  /* If lf->extra is non-empty, return it (display name for item).
   * Otherwise default to item name. */
  {
  struct linkedFeatures *lf = item;
  char *extra = (char *)lf->extra;
  if (isNotEmpty(extra))
      {
      static char displayName[512];
      safecpy(displayName, sizeof(displayName), extra);
      char *ptr = strchr(displayName, '|');
      if (ptr != NULL)
  	*ptr = '\0';
      if (isNotEmpty(displayName))
  	return displayName;
      }
  return lf->name;
  }
  
  struct track *pcrResultTg()
  /* Make track of hgPcr results (alignments of user's submitted primers). */
  {
  struct trackDb *tdb = pcrResultFakeTdb();
  struct track *tg = trackFromTrackDb(tdb);
  tg->loadItems = pcrResultLoad;
  tg->itemName = pcrResultTrackItemName;
  tg->mapItemName = pcrResultMapItemName;
  tg->exonArrows = TRUE;
  tg->hasUi = TRUE;
  return tg;
  }
  
  struct track *linkedFeaturesTg()
  /* Return generic track for linked features. */
  {
  struct track *tg = trackNew();
  linkedFeaturesMethods(tg);
  tg->colorShades = shadesOfGray;
  return tg;
  }
  
  void setTgDarkLightColors(struct track *tg, int r, int g, int b)
  /* Set track color to r,g,b.  Set altColor to a lighter version
   * of the same. */
  {
  tg->colorShades = NULL;
  tg->color.r = r;
  tg->color.g = g;
  tg->color.b = b;
  tg->altColor.r = (r+255)/2;
  tg->altColor.g = (g+255)/2;
  tg->altColor.b = (b+255)/2;
  }
  
  void parseSs(char *ss, char **retPsl, char **retFa)
  /* Parse out ss variable into components. */
  {
  static char buf[1024];
  char *words[2];
  int wordCount;
  
  safecpy(buf, sizeof(buf), ss);
  wordCount = chopLine(buf, words);
  if (wordCount < 2)
      errAbort("Badly formated ss variable");
  *retPsl = words[0];
  *retFa = words[1];
  }
  
  boolean ssFilesExist(char *ss)
  /* Return TRUE if both files in ss exist. */
  {
  char *faFileName, *pslFileName;
  parseSs(ss, &pslFileName, &faFileName);
  return fileExists(pslFileName) && fileExists(faFileName);
  }
  
  void loadUserPsl(struct track *tg)
  /* Load up hgBlat results from table into track items. */
  {
  char *ss = userSeqString;
  char buf2[3*512];
  char *faFileName, *pslFileName;
  struct lineFile *f;
  struct psl *psl;
  struct linkedFeatures *lfList = NULL, *lf;
  enum gfType qt, tt;
  int sizeMul = 1;
  
  parseSs(ss, &pslFileName, &faFileName);
  pslxFileOpen(pslFileName, &qt, &tt, &f);
  if (qt == gftProt)
      {
      setTgDarkLightColors(tg, 0, 80, 150);
      tg->colorShades = NULL;
      sizeMul = 3;
      }
  tg->itemName = linkedFeaturesName;
  while ((psl = pslNext(f)) != NULL)
      {
      if (sameString(psl->tName, chromName) && psl->tStart < winEnd && psl->tEnd > winStart)
  	{
  	lf = lfFromPslx(psl, sizeMul, TRUE, FALSE, tg);
  	sprintf(buf2, "%s %s", ss, psl->qName);
  	lf->extra = cloneString(buf2);
  	slAddHead(&lfList, lf);
  	/* Don't free psl -- used in drawing phase by baseColor code. */
  	}
      else
  	pslFree(&psl);
      }
  slSort(&lfList, linkedFeaturesCmpStart);
  lineFileClose(&f);
  tg->items = lfList;
  }
  
  static void addUserSeqBaseAndIndelSettings(struct trackDb *tdb)
  /* If user sequence is a dna or rna alignment, add settings to enable
   * base-level differences and indel display. */
  {
  enum gfType qt, tt;
  struct lineFile *lf;
  char *faFileName, *pslFileName;
  parseSs(userSeqString, &pslFileName, &faFileName);
  pslxFileOpen(pslFileName, &qt, &tt, &lf);
  lineFileClose(&lf);
  if (qt != gftProt)
      {
      if (tdb->settingsHash == NULL)
  	tdb->settingsHash = hashNew(0);
      hashAdd(tdb->settingsHash, BASE_COLOR_DEFAULT, cloneString("diffBases"));
      hashAdd(tdb->settingsHash, BASE_COLOR_USE_SEQUENCE, cloneString("ss"));
      hashAdd(tdb->settingsHash, SHOW_DIFF_BASES_ALL_SCALES, cloneString("."));
      hashAdd(tdb->settingsHash, INDEL_DOUBLE_INSERT, cloneString("on"));
      hashAdd(tdb->settingsHash, INDEL_QUERY_INSERT, cloneString("on"));
      hashAdd(tdb->settingsHash, INDEL_POLY_A, cloneString("on"));
      }
  }
  
  struct track *userPslTg()
  /* Make track of user pasted sequence. */
  {
  struct track *tg = linkedFeaturesTg();
  struct trackDb *tdb;
  tg->track = "hgUserPsl";
  tg->table = tg->track;
  tg->canPack = TRUE;
  tg->visibility = tvPack;
  tg->longLabel = "Your Sequence from Blat Search";
  tg->shortLabel = "Blat Sequence";
  tg->loadItems = loadUserPsl;
  tg->mapItemName = lfMapNameFromExtra;
  tg->priority = 103;
  tg->defaultPriority = tg->priority;
  tg->groupName = "map";
  tg->defaultGroupName = cloneString(tg->groupName);
  tg->exonArrows = TRUE;
  
  /* better to create the tdb first, then use trackFromTrackDb */
  AllocVar(tdb);
  tdb->track = cloneString(tg->track);
  tdb->table = cloneString(tg->table);
  tdb->visibility = tg->visibility;
  tdb->shortLabel = cloneString(tg->shortLabel);
  tdb->longLabel = cloneString(tg->longLabel);
  tdb->grp = cloneString(tg->groupName);
  tdb->priority = tg->priority;
  tdb->type = cloneString("psl");
  tdb->canPack = tg->canPack;
  trackDbPolish(tdb);
  addUserSeqBaseAndIndelSettings(tdb);
  tg->tdb = tdb;
  return tg;
  }
  
  char *oligoMatchSeq()
  /* Return sequence for oligo matching. */
  {
  char *s = cartOptionalString(cart, oligoMatchVar);
  if (s != NULL)
      {
      int len;
      tolowers(s);
      iupacFilter(s, s);
      len = strlen(s);
      if (len < 2)
         s = NULL;
      }
  if (s == NULL)
      s = cloneString(oligoMatchDefault);
  return s;
  }
  
  char *oligoMatchName(struct track *tg, void *item)
  /* Return name for oligo, which is just the base position. */
  {
  struct bed *bed = item;
  static char buf[22];
  buf[0] = bed->strand[0];
  sprintLongWithCommas(buf+1, bed->chromStart+1);
  return buf;
  }
  
  char *dnaInWindow()
  /* This returns the DNA in the window, all in lower case. */
  {
  static struct dnaSeq *seq = NULL;
  if (seq == NULL)
      seq = hDnaFromSeq(database, chromName, winStart, winEnd, dnaLower);
  return seq->dna;
  }
  
  char *stringInWrapper(char *needle, char *haystack)
  /* Wrapper around string in to make it so it's a function rather than a macro. */
  {
  return stringIn(needle, haystack);
  }
  
  void oligoMatchLoad(struct track *tg)
  /* Create track of perfect matches to oligo on either strand.
   *
   * Note that if you are extending this code, there is also a parallel copy
   * in src/hg/utils/oligoMatch/ that should be kept up to date!
   *
   */
  {
  char *dna = dnaInWindow();
  char *fOligo = oligoMatchSeq();
  char *(*finder)(char *needle, char *haystack) = (anyIupac(fOligo) ? iupacIn : stringInWrapper);
  int oligoSize = strlen(fOligo);
  char *rOligo = cloneString(fOligo);
  char *rMatch = NULL, *fMatch = NULL;
  struct bed *bedList = NULL, *bed;
  char strand;
  int count = 0, maxCount = 1000000;
  
  if (oligoSize >= 2)
      {
      fMatch = finder(fOligo, dna);
      iupacReverseComplement(rOligo, oligoSize);
      if (sameString(rOligo, fOligo))
          rOligo = NULL;
      else
  	rMatch = finder(rOligo, dna);
      for (;;)
          {
  	char *oneMatch = NULL;
  	if (rMatch == NULL)
  	    {
  	    if (fMatch == NULL)
  		break;
  	    else
  		{
  		oneMatch = fMatch;
  		fMatch = finder(fOligo, fMatch+1);
  		strand = '+';
  		}
  	    }
  	else if (fMatch == NULL)
  	    {
  	    oneMatch = rMatch;
  	    rMatch = finder(rOligo, rMatch+1);
  	    strand = '-';
  	    }
  	else if (rMatch < fMatch)
  	    {
  	    oneMatch = rMatch;
  	    rMatch = finder(rOligo, rMatch+1);
  	    strand = '-';
  	    }
  	else
  	    {
  	    oneMatch = fMatch;
  	    fMatch = finder(fOligo, fMatch+1);
  	    strand = '+';
  	    }
  	if (count < maxCount)
  	    {
  	    ++count;
  	    AllocVar(bed);
  	    bed->chromStart = winStart + (oneMatch - dna);
  	    bed->chromEnd = bed->chromStart + oligoSize;
  	    bed->strand[0] = strand;
  	    slAddHead(&bedList, bed);
  	    }
  	else
  	    break;
  	}
      slReverse(&bedList);
      if (count < maxCount)
  	tg->items = bedList;
      else
          warn("More than %d items in %s, suppressing display", maxCount, tg->shortLabel);
      }
  }
  
  struct track *oligoMatchTg()
  /* Make track of perfect matches to oligomer. */
  {
  struct track *tg = trackNew();
  char *oligo = oligoMatchSeq();
  int oligoSize = strlen(oligo);
  char *medOligo = cloneString(oligo);
  static char longLabel[80];
  struct trackDb *tdb;
  
  /* Generate abbreviated strings. */
  if (oligoSize >= 30)
      {
      memset(medOligo + 30-3, '.', 3);
      medOligo[30] = 0;
      }
  touppers(medOligo);
  
  bedMethods(tg);
  tg->track = "oligoMatch";
  tg->table = tg->track;
  tg->canPack = TRUE;
  tg->visibility = tvHide;
  tg->hasUi = TRUE;
  tg->shortLabel = cloneString(OLIGO_MATCH_TRACK_LABEL);
  safef(longLabel, sizeof(longLabel),
        "Perfect Matches to Short Sequence (%s)", medOligo);
  tg->longLabel = longLabel;
  tg->loadItems = oligoMatchLoad;
  tg->itemName = oligoMatchName;
  tg->mapItemName = oligoMatchName;
  tg->priority = OLIGO_MATCH_TRACK_PRIORITY;
  tg->defaultPriority = tg->priority;
  tg->groupName = "map";
  tg->defaultGroupName = cloneString(tg->groupName);
  
  AllocVar(tdb);
  tdb->track = cloneString(tg->track);
  tdb->table = cloneString(tg->table);
  tdb->visibility = tg->visibility;
  tdb->shortLabel = cloneString(tg->shortLabel);
  tdb->longLabel = cloneString(tg->longLabel);
  tdb->grp = cloneString(tg->groupName);
  tdb->priority = tg->priority;
  tdb->canPack = tg->canPack;
  trackDbPolish(tdb);
  tg->tdb = tdb;
  return tg;
  }
  
  Color maybeDarkerLabels(struct track *track, struct hvGfx *hvg, Color color)
  /* For tracks having light track display but needing a darker label */
  {
  if (trackDbSetting(track->tdb, "darkerLabels"))
      {
      struct hsvColor hsv = mgRgbToHsv(mgColorIxToRgb(NULL, color));
      // check if really pale
      if (hsv.s < 500 ||(hsv.h > 40.0 && hsv.h < 150.0))
          return somewhatDarkerColor(hvg, color);
      return slightlyDarkerColor(hvg, color);
      }
  return color;
  }
  
  boolean isCenterLabelsPackOff(struct track *track)
  /* Check for trackDb setting to suppress center labels of composite in pack mode */
  {
  if (!track || !track->tdb)
      return FALSE;
  char *centerLabelsPack = trackDbSetting(track->tdb, "centerLabelsPack");
  return (centerLabelsPack && sameWord(centerLabelsPack, "off"));
  }
  
  static int doLeftLabels(struct track *track, struct hvGfx *hvg, MgFont *font,
                                  int y)
  /* Draw left labels.  Return y coord. */
  {
  struct slList *prev = NULL;
  
  /* for sample tracks */
  double minRangeCutoff, maxRangeCutoff;
  double minRange, maxRange;
  double min0, max0;
  char minRangeStr[32];
  char maxRangeStr[32];
  
  int ymin, ymax;
  int newy;
  char o4[256];
  char o5[256];
  struct slList *item;
  enum trackVisibility vis = track->limitedVis;
  Color labelColor = (track->labelColor ?
                          track->labelColor : track->ixColor);
  labelColor = maybeDarkerLabels(track, hvg, labelColor);
  int fontHeight = mgFontLineHeight(font);
  int tHeight = trackPlusLabelHeight(track, fontHeight);
  if (vis == tvHide)
      return y;
  
  /*  if a track can do its own left labels, do them after drawItems */
  if (track->drawLeftLabels != NULL)
      return y + tHeight;
  
  /*  Wiggle tracks depend upon clipping.  They are reporting
   *  totalHeight artifically high by 1 so this will leave a
   *  blank area one pixel high below the track.
   */
  if (sameString("wig",track->tdb->type) || sameString("bedGraph",track->tdb->type))
      hvGfxSetClip(hvg, leftLabelX, y, leftLabelWidth, tHeight-1);
  else
      hvGfxSetClip(hvg, leftLabelX, y, leftLabelWidth, tHeight);
  
  minRange = 0.0;
  safef( o4, sizeof(o4),"%s.min.cutoff", track->track);
  safef( o5, sizeof(o5),"%s.max.cutoff", track->track);
  minRangeCutoff = max( atof(cartUsualString(cart,o4,"0.0"))-0.1,
                                  track->minRange );
  maxRangeCutoff = min( atof(cartUsualString(cart,o5,"1000.0"))+0.1,
                                  track->maxRange);
  if( sameString( track->table, "humMusL" ) ||
      sameString( track->table, "musHumL" ) ||
      sameString( track->table, "mm3Rn2L" ) ||
      sameString( track->table, "hg15Mm3L" ) ||
      sameString( track->table, "mm3Hg15L" ) ||
      sameString( track->table, "regpotent" ) ||
      sameString( track->table, "HMRConservation" )  )
      {
      int binCount = round(1.0/track->scaleRange);
      minRange = whichSampleBin( minRangeCutoff, track->minRange, track->maxRange, binCount );
      maxRange = whichSampleBin( maxRangeCutoff, track->minRange, track->maxRange, binCount );
      min0 = whichSampleNum( minRange, track->minRange,track->maxRange, binCount );
      max0 = whichSampleNum( maxRange, track->minRange, track->maxRange, binCount );
      sprintf( minRangeStr, " "  );
      sprintf( maxRangeStr, " " );
      if( vis == tvFull && track->heightPer >= 74  )
          {
          samplePrintYAxisLabel( hvg, y+5, track, "1.0", min0, max0 );
          samplePrintYAxisLabel( hvg, y+5, track, "2.0", min0, max0 );
          samplePrintYAxisLabel( hvg, y+5, track, "3.0", min0, max0 );
          samplePrintYAxisLabel( hvg, y+5, track, "4.0", min0, max0 );
          samplePrintYAxisLabel( hvg, y+5, track, "5.0", min0, max0 );
          samplePrintYAxisLabel( hvg, y+5, track, "6.0", min0, max0 );
          }
      }
  else
      {
      sprintf( minRangeStr, "%d", (int)round(minRangeCutoff));
      sprintf( maxRangeStr, "%d", (int)round(maxRangeCutoff));
      }
  /* special label handling for wigMaf type tracks -- they
     display a left label in pack mode.  To use the full mode
     labeling, temporarily set visibility to full.
     Restore savedVis later */
  if (startsWith("bigMaf", track->tdb->type) || startsWith("wigMaf", track->tdb->type) || startsWith("maf", track->tdb->type))
      vis = tvFull;
  /* behave temporarily like pack for these */
  if (track->limitedVis == tvFull && isTypeBedLike(track))
      vis = tvPack;
  
  switch (vis)
      {
      case tvHide:
          break;  /* Do nothing; */
      case tvPack:
          if (isCenterLabelsPackOff(track))
              // draw left labels for pack mode track with center labels off
              {
              if (isCenterLabelIncluded(track))
                  y += fontHeight;
              hvGfxTextRight(hvg, leftLabelX, y, leftLabelWidth-1, track->lineHeight, labelColor, font, 
                                  track->shortLabel);
              y += track->height;
              }
          else
              y += tHeight;
          break;
      case tvSquish:
  	y += tHeight;
          break;
      case tvFull:
          if (isCenterLabelIncluded(track))
              y += fontHeight;
  
          if( track->subType == lfSubSample && track->items == NULL )
              y += track->height;
  
          for (item = track->items; item != NULL; item = item->next)
              {
              char *rootName;
              char *name = track->itemName(track, item);
              int itemHeight = track->itemHeight(track, item);
              //warn(" track %s, itemHeight %d\n", track->shortLabel, itemHeight);
              newy = y;
  
              if (track->itemLabelColor != NULL)
                  labelColor = track->itemLabelColor(track, item, hvg);
              else
                  labelColor = maybeDarkerLabels(track, hvg, labelColor);
  
              /* Do some fancy stuff for sample tracks.
               * Draw y-value limits for 'sample' tracks. */
              if (track->subType == lfSubSample )
                  {
                  if( prev == NULL )
                      newy += itemHeight;
                  else
                      newy += sampleUpdateY(name,
                              track->itemName(track, prev), itemHeight);
                  if( newy == y )
                      continue;
  
                  if( track->heightPer > (3 * fontHeight ) )
                      {
                      ymax = y - (track->heightPer / 2) + (fontHeight / 2);
                      ymin = y + (track->heightPer / 2) - (fontHeight / 2);
                      hvGfxTextRight(hvg, leftLabelX, ymin, leftLabelWidth-1,
                                  itemHeight, track->ixAltColor,
                                  font, minRangeStr );
                      hvGfxTextRight(hvg, leftLabelX, ymax, leftLabelWidth-1,
                                  itemHeight, track->ixAltColor,
                                  font, maxRangeStr );
                      }
                  prev = item;
  
                  rootName = cloneString( name );
                  beforeFirstPeriod( rootName );
                  if( sameString( track->table, "humMusL" ) ||
                           sameString( track->table, "hg15Mm3L" ))
                      hvGfxTextRight(hvg, leftLabelX, y, leftLabelWidth - 1,
                               itemHeight, track->ixColor, font, "Mouse Cons");
                  else if( sameString( track->table, "musHumL" ) ||
                           sameString( track->table, "mm3Hg15L"))
                      hvGfxTextRight(hvg, leftLabelX, y, leftLabelWidth - 1,
                                  itemHeight, track->ixColor, font, "Human Cons");
                  else if( sameString( track->table, "mm3Rn2L" ))
                      hvGfxTextRight(hvg, leftLabelX, y, leftLabelWidth - 1,
                                  itemHeight, track->ixColor, font, "Rat Cons");
                  else
                      hvGfxTextRight(hvg, leftLabelX, y, leftLabelWidth - 1,
                                  itemHeight, track->ixColor, font, rootName );
                  freeMem( rootName );
                  y = newy;
                  }
              else
                  {
                  /* standard item labeling */
  		if (highlightItem(track, item))
  		    {
  		    int nameWidth = mgFontStringWidth(font, name);
  		    int boxStart = leftLabelX + leftLabelWidth - 2 - nameWidth;
  		    hvGfxBox(hvg, boxStart, y, nameWidth+1, itemHeight - 1,
  		      labelColor);
  		    hvGfxTextRight(hvg, leftLabelX, y, leftLabelWidth-1,
  			itemHeight, MG_WHITE, font, name);
  		    }
  		else
  		    hvGfxTextRight(hvg, leftLabelX, y, leftLabelWidth - 1,
  			itemHeight, labelColor, font, name);
  		y += itemHeight;
  		}
              }
          break;
      case tvDense:
  
          if (isCenterLabelIncluded(track))
              y += fontHeight;
  
          /*draw y-value limits for 'sample' tracks.
           * (always puts 0-100% range)*/
          if (track->subType == lfSubSample && track->heightPer > (3 * fontHeight))
              {
              int ymax = y - (track->heightPer / 2) + (fontHeight / 2);
              int ymin = y + (track->heightPer / 2) - (fontHeight / 2);
              hvGfxTextRight(hvg, leftLabelX, ymin,
                          leftLabelWidth-1, track->lineHeight,
                          track->ixAltColor, font, minRangeStr );
              hvGfxTextRight(hvg, leftLabelX, ymax,
                          leftLabelWidth-1, track->lineHeight,
                          track->ixAltColor, font, maxRangeStr );
              }
          hvGfxTextRight(hvg, leftLabelX, y, leftLabelWidth-1,
                      track->lineHeight, labelColor, font,
                      track->shortLabel);
          y += track->height;
          break;
      }
  hvGfxUnclip(hvg);
  return y;
  }
  
  void setGlobalsFromWindow(struct window *window);  // FORWARD DECLARATION
  
  static void doLabelNextItemButtons(struct track *track, struct track *parentTrack,
                                     struct hvGfx *hvg, MgFont *font, int y,
                                     int trackPastTabX, int trackPastTabWidth, int fontHeight,
                                     int insideHeight, Color labelColor)
  /* If the track allows label next-item buttons (next gene), draw them. */
  /* The button will cause hgTracks to run again with the additional CGI */
  /* vars nextItem=trackName or prevItem=trackName, which will then  */
  /* signal the browser to find the next thing on the track before it */
  /* does anything else. */
  {
  int portWidth = fullInsideWidth;
  int portX = fullInsideX;
  // If a portal was established, then set the portal dimensions
  long portalStart,chromStart;
  double basesPerPixel;
  if (theImgBox
  && imgBoxPortalDimensions(theImgBox,&chromStart,NULL,NULL,NULL,&portalStart,NULL,
                            &portWidth,&basesPerPixel))
      {
      portX = (int)((portalStart - chromStart) / basesPerPixel);
      portX += gfxBorder;
      if (withLeftLabels)
          portX += tl.leftLabelWidth + gfxBorder;
      portWidth = portWidth-gfxBorder-insideX;
      }
  int arrowWidth = insideHeight;
  int arrowButtonWidth = arrowWidth + 2 * NEXT_ITEM_ARROW_BUFFER;
  int rightButtonX = portX + portWidth - arrowButtonWidth - 1;
  char buttonText[256];
  Color fillColor = lightGrayIndex();
  labelColor = blackIndex();
  hvGfxNextItemButton(hvg, rightButtonX + NEXT_ITEM_ARROW_BUFFER, y, arrowWidth, arrowWidth,
                      labelColor, fillColor, TRUE);
  hvGfxNextItemButton(hvg, portX + NEXT_ITEM_ARROW_BUFFER, y, arrowWidth, arrowWidth,
                      labelColor, fillColor, FALSE);
  
  safef(buttonText, ArraySize(buttonText), "hgt.prevItem=%s", track->track);
  
  mapBoxReinvoke(hvg, portX, y + 1, arrowButtonWidth, insideHeight, track, FALSE,
                 NULL, 0, 0, (revCmplDisp ? "Next item" : "Prev item"), buttonText);
  
  #ifdef IMAGEv2_SHORT_TOGGLE
  char *label = (theImgBox ? track->longLabel : parentTrack->longLabel);
  int width = portWidth - (2 * arrowButtonWidth);
  int x = portX + arrowButtonWidth;
  // make toggle cover only actual label
  int size = mgFontStringWidth(font,label) + 12;  // get close enough to the label
  if (width > size)
      {
      x += width/2 - size/2;
      width = size;
      }
  mapBoxToggleVis(hvg, x, y + 1, width, insideHeight, (theImgBox ? track : parentTrack));
  #else///ifndef IMAGEv2_SHORT_TOGGLE
  mapBoxToggleVis(hvg, portX + arrowButtonWidth, y + 1, portWidth - (2 * arrowButtonWidth),
                  insideHeight, (theImgBox ? track : parentTrack));
  #endif///ndef IMAGEv2_SHORT_TOGGLE
  
  // use the last window globals instead of the first
  struct window *w=windows;
  while(w->next)
      w = w->next;
  setGlobalsFromWindow(w); // use last window
  
  safef(buttonText, ArraySize(buttonText), "hgt.nextItem=%s", track->track);
  mapBoxReinvoke(hvg, portX + portWidth - arrowButtonWidth, y + 1, arrowButtonWidth, insideHeight,
                 track, FALSE, NULL, 0, 0, (revCmplDisp ? "Prev item" : "Next item"), buttonText);
  
  setGlobalsFromWindow(windows); // restore first window
  
  }
  
  static int doCenterLabels(struct track *track, struct track *parentTrack,
                                  struct hvGfx *hvg, MgFont *font, int y, int fullInsideWidth )
  /* Draw center labels.  Return y coord */
  {
  if (track->limitedVis != tvHide)
      {
      if (isCenterLabelIncluded(track))
          {
          int trackPastTabX = (withLeftLabels ? trackTabWidth : 0);
          int trackPastTabWidth = tl.picWidth - trackPastTabX;
          int fontHeight = mgFontLineHeight(font);
          int insideHeight = fontHeight-1;
  	boolean toggleDone = FALSE;
          char *label = track->longLabel;
          Color labelColor = (track->labelColor ?
                              track->labelColor : track->ixColor);
          if (isCenterLabelConditional(track))
              {
              struct trackDb* tdbComposite = tdbGetComposite(track->tdb);
              if (tdbComposite != NULL)
                  {
                  label = tdbComposite->longLabel;
                  labelColor = hvGfxFindColorIx(hvg, tdbComposite->colorR,
                                                  tdbComposite->colorG, tdbComposite->colorB);
                  }
              }
          labelColor = maybeDarkerLabels(track, hvg, labelColor);
          hvGfxTextCentered(hvg, insideX, y+1, fullInsideWidth, insideHeight,
                            labelColor, font, label);
          if (track->nextItemButtonable && track->nextPrevItem && !tdbIsComposite(track->tdb))
              {
              if (withNextItemArrows || trackDbSettingOn(track->tdb, "nextItemButton"))
                  {
                  doLabelNextItemButtons(track, parentTrack, hvg, font, y, trackPastTabX,
                                         trackPastTabWidth, fontHeight, insideHeight, labelColor);
                  toggleDone = TRUE;
                  }
              }
          if (!toggleDone)
              {
          #ifdef IMAGEv2_SHORT_TOGGLE
              // make toggle cover only actual label
              int size = mgFontStringWidth(font,label) + 12;  // get close enough to the label
              if (trackPastTabWidth > size)
                  {
                  trackPastTabX = insideX + insideWidth/2 - size/2;
                  trackPastTabWidth = size;
                  }
          #endif///def IMAGEv2_SHORT_TOGGLE
              mapBoxToggleVis(hvg, trackPastTabX, y+1,trackPastTabWidth, insideHeight,
                              (theImgBox ? track : parentTrack));
              }
          y += fontHeight;
          }
      y += track->totalHeight(track, track->limitedVis);
      }
  return y;
  }
  
  static void doPreDrawItems(struct track *track, struct hvGfx *hvg, MgFont *font,
                                      int y, long *lastTime)
  /* Do Pre-Draw track items. */
  {
  int fontHeight = mgFontLineHeight(font);
  if (isCenterLabelIncluded(track))
      y += fontHeight;
  if (track->preDrawItems)
      track->preDrawItems(track, winStart, winEnd, hvg, insideX, y, insideWidth,
                   font, track->ixColor, track->limitedVis);
  if (measureTiming && lastTime)
      {
      long thisTime = clock1000();
      track->drawTime = thisTime - *lastTime;
      *lastTime = thisTime;
      }
  }
  
  static int doDrawItems(struct track *track, struct hvGfx *hvg, MgFont *font,
                                      int y, long *lastTime)
  /* Draw track items.  Return y coord */
  {
  int fontHeight = mgFontLineHeight(font);
  int pixWidth = tl.picWidth;
  if (isCenterLabelIncluded(track))
      y += fontHeight;
  if (track->limitedVis == tvPack)
      {
      hvGfxSetClip(hvg, gfxBorder+trackTabWidth+1, y,
          pixWidth-2*gfxBorder-trackTabWidth-1, track->height);
      }
  else
      hvGfxSetClip(hvg, insideX, y, insideWidth, track->height);
  track->drawItems(track, winStart, winEnd, hvg, insideX, y, insideWidth,
                   font, track->ixColor, track->limitedVis);
  if (measureTiming && lastTime)
      {
      long thisTime = clock1000();
      track->drawTime = thisTime - *lastTime;
      *lastTime = thisTime;
      }
  hvGfxUnclip(hvg);
  y += track->totalHeight(track, track->limitedVis);
  return y;
  }
  
  static int doMapItems(struct track *track, struct hvGfx *hvg, int fontHeight, int y)
  /* Draw map boxes around track items */
  {
  char *type = track->tdb->type;
  int newy;
  int trackPastTabX = (withLeftLabels ? trackTabWidth : 0);
  int trackPastTabWidth = tl.picWidth - trackPastTabX;
  int start = 1;
  struct slList *item;
  boolean isWig = (sameString("wig", type) || startsWith("wig ", type) ||
                   startsWith("bedGraph", type));
  
  if (isCenterLabelIncluded(track))
      y += fontHeight;
  if (track->mapsSelf)
      {
      return y+track->height;
      }
  if (track->subType == lfSubSample && track->items == NULL)
       y += track->lineHeight;
  
  /* override doMapItems for hapmapLd track */
  /* does not scale with subtracks right now, so this is commented out until it can be fixed
  if (startsWith("hapmapLd",track->table))
      {
      y += round((double)(scaleForPixels(insideWidth)*insideWidth/2));
      return y;
      }
  */
  for (item = track->items; item != NULL; item = item->next)
      {
      int height = track->itemHeight(track, item);
  
      /*wiggle tracks don't always increment height (y-value) here*/
      if( track->subType == lfSubSample )
          {
          newy = y;
          if( !start && item->next != NULL  )
              {
              newy += sampleUpdateY( track->itemName(track, item),
                               track->itemName(track, item->next),
                               height );
              }
          else if( item->next != NULL || start )
              newy += height;
          start = 0;
          y = newy;
          }
      else
          {
  	if (track->mapItem == NULL)
  	    track->mapItem = genericMapItem;
          if (!track->mapsSelf)
              {
  	    track->mapItem(track, hvg, item, track->itemName(track, item),
                             track->mapItemName(track, item),
                             track->itemStart(track, item),
                             track->itemEnd(track, item), trackPastTabX,
                             y, trackPastTabWidth,height);
              }
          y += height;
          }
      }
  /* Wiggle track's ->height is actually one less than what it returns from
   * totalHeight()... I think the least disruptive way to account for this
   * (and not touch Ryan Weber's Sample stuff) is to just correct here if
   * we see wiggle or bedGraph: */
  if (isWig)
      y++;
  return y;
  }
  
  static int doOwnLeftLabels(struct track *track, struct hvGfx *hvg,
                                                  MgFont *font, int y)
  /* Track draws it own, custom left labels */
  {
  int fontHeight = mgFontLineHeight(font);
  int tHeight = trackPlusLabelHeight(track, fontHeight);
  Color labelColor = (track->labelColor ? track->labelColor : track->ixColor);
  labelColor = maybeDarkerLabels(track, hvg, labelColor);
  hvGfxSetClip(hvg, leftLabelX, y, leftLabelWidth, tHeight);
  track->drawLeftLabels(track, winStart, winEnd,
  		      hvg, leftLabelX, y, leftLabelWidth, tHeight,
                        isCenterLabelIncluded(track), font, labelColor,
  		      track->limitedVis);
  hvGfxUnclip(hvg);
  y += tHeight;
  return y;
  }
  
  // defined below:
  static int getMaxWindowToDraw(struct trackDb *tdb);
  
  int doTrackMap(struct track *track, struct hvGfx *hvg, int y, int fontHeight,
                 int trackPastTabX, int trackPastTabWidth)
  /* Write out the map for this track. Return the new offset. */
  {
  int mapHeight = 0;
  switch (track->limitedVis)
      {
      case tvPack:
      case tvSquish:
          y += trackPlusLabelHeight(track, fontHeight);
          break;
      case tvFull:
          if (!nextItemCompatible(track))
              {
              if (trackIsCompositeWithSubtracks(track))  // TODO: Change when tracks->subtracks
                  {                                      //       are always set for composite
                  if (isCenterLabelIncluded(track))
                      y += fontHeight;
                  struct track *subtrack;
                  for (subtrack = track->subtracks;  subtrack != NULL;subtrack = subtrack->next)
                      {
                      if (isSubtrackVisible(subtrack))
                          {
                          if (subtrack->limitedVis == tvFull)
                              y = doMapItems(subtrack, hvg, fontHeight, y);
                          else
                              {
                              if (isCenterLabelIncluded(subtrack))
                                  y += fontHeight;
                              if (theImgBox && subtrack->limitedVis == tvDense)
                                  mapBoxToggleVis(hvg, trackPastTabX, y, trackPastTabWidth,
                                                  track->lineHeight, subtrack);
                              y += subtrack->totalHeight(subtrack, subtrack->limitedVis);
                              }
                          }
                      }
                  }
              else
  		y = doMapItems(track, hvg, fontHeight, y);
              }
          else
              y += trackPlusLabelHeight(track, fontHeight);
          break;
      case tvDense:
          if (isCenterLabelIncluded(track))
              y += fontHeight;
          if (tdbIsComposite(track->tdb))
              mapHeight = track->height;
          else
              mapHeight = track->lineHeight;
          int maxWinToDraw = getMaxWindowToDraw(track->tdb);
          if (maxWinToDraw <= 1 || (winEnd - winStart) <= maxWinToDraw)
              mapBoxToggleVis(hvg, trackPastTabX, y, trackPastTabWidth, mapHeight, track);
          y += mapHeight;
          break;
      case tvHide:
      default:
          break;  /* Do nothing; */
      }
  return y;
  }
  
  long computeScaleBar(long numBases, char scaleText[], int scaleTextSize)
  /* Do some scalebar calculations and return the number of bases the scalebar will span. */
  {
  char *baseWord = "bases";
  long scaleBases = 0;
  int scaleBasesTextNum = 0;
  int numFigs = (int)log10(numBases);
  int frontNum = (int)(numBases/pow(10,numFigs));
  if (frontNum == 1)
      {
      numFigs--;
      scaleBases = 5 * pow(10, numFigs);
      scaleBasesTextNum = 5 * pow(10, numFigs % 3);
      }
  else if ((frontNum > 1) && (frontNum <= 4))
      {
      scaleBases = pow(10, numFigs);
      scaleBasesTextNum = pow(10, numFigs % 3);
      }
  else if (frontNum > 4)
      {
      scaleBases = 2 * pow(10, numFigs);
      scaleBasesTextNum = 2 * pow(10, numFigs % 3);
      }
  if ((numFigs >= 3) && (numFigs < 6))
      baseWord = "kb";
  else if ((numFigs >= 6) && (numFigs < 9))
      baseWord = "Mb";
  else if ((numFigs >= 9) && (numFigs < 12))
      baseWord = "Gb";
  safef(scaleText, scaleTextSize, "%d %s", scaleBasesTextNum, baseWord);
  return scaleBases;
  }
  
  enum trackVisibility limitedVisFromComposite(struct track *subtrack)
  /* returns the subtrack visibility which may be limited by composite with multi-view dropdowns. */
  {
  if (tdbIsCompositeChild(subtrack->tdb))
      {
      if (!subtrack->limitedVisSet)
          {
          subtrack->visibility = tdbVisLimitedByAncestors(cart, subtrack->tdb, TRUE, TRUE);
          limitVisibility(subtrack);
          }
      }
  else
      limitVisibility(subtrack);
  return subtrack->limitedVis;
  }
  
  static int calcNewWinWidth(struct cart *cart, int winStart, int winEnd, int insideWidth)
  /* Calc width of hit boxes that will zoom program around ruler. */
  // TODO GALT
  // probably should change this to use full and virt vars and make it
  // go across the entire full image width. Especially since this is about
  // zooming on the virt chrom when user clicks on scalebar or chrom-ruler
  // Default is to just zoom current window in by 3x to 1/3 of current width.
  {
  int winWidth = winEnd - winStart;
  int newWinWidth = winWidth;
  char message[32];
  char *zoomType = cartCgiUsualString(cart, RULER_BASE_ZOOM_VAR, ZOOM_3X);
  
  safef(message, sizeof(message), "%s zoom", zoomType);
  if (sameString(zoomType, ZOOM_1PT5X))
      newWinWidth = winWidth/1.5;
  else if (sameString(zoomType, ZOOM_3X))
      newWinWidth = winWidth/3;
  else if (sameString(zoomType, ZOOM_10X))
      newWinWidth = winWidth/10;
  else if (sameString(zoomType, ZOOM_100X))
      newWinWidth = winWidth/100;
  else if (sameString(zoomType, ZOOM_BASE))
      newWinWidth = insideWidth/tl.mWidth;
  else
      errAbort("invalid zoom type %s", zoomType);
  
  if (newWinWidth < 1)
      newWinWidth = 1;
  
  return newWinWidth;
  }
  
  static void drawScaleBar(
      struct hvGfx *hvg,
      MgFont *font,
      int fontHeight,
      int yAfterRuler,
      int y,
      int scaleBarTotalHeight
      )
  /* draws the scale bar */
  {
  int scaleBarPad = 2;
  int scaleBarHeight = fontHeight;
  
  // can have one for entire multi-window image
  char scaleText[32];
  long numBases = 0;
  struct window *w;
  for (w=windows; w; w=w->next)
      numBases += (w->winEnd - w->winStart);
  long scaleBases = computeScaleBar(numBases, scaleText, sizeof(scaleText));
  int scalePixels = (int)((double)fullInsideWidth*scaleBases/numBases);
  int scaleBarX = fullInsideX + (int)(((double)fullInsideWidth-scalePixels)/2);
  int scaleBarEndX = scaleBarX + scalePixels;
  int scaleBarY = y + 0.5 * scaleBarTotalHeight;
  hvGfxTextRight(hvg, fullInsideX, y + scaleBarPad,
  	       (scaleBarX-2)-fullInsideX, scaleBarHeight, MG_BLACK, font, scaleText);
  hvGfxLine(hvg, scaleBarX, scaleBarY, scaleBarEndX, scaleBarY, MG_BLACK);
  hvGfxLine(hvg, scaleBarX, y+scaleBarPad, scaleBarX,
  	  y+scaleBarTotalHeight-scaleBarPad, MG_BLACK);
  hvGfxLine(hvg, scaleBarEndX, y+scaleBarPad, scaleBarEndX,
  	  y+scaleBarTotalHeight-scaleBarPad, MG_BLACK);
  if(cartUsualBoolean(cart, BASE_SHOWASM_SCALEBAR, TRUE))
      {
      int fHeight = vgGetFontPixelHeight(hvg->vg, font);
      hvGfxText(hvg, scaleBarEndX + 10,
  	      y + (scaleBarTotalHeight - fHeight)/2 + ((font == mgSmallFont()) ?  1 : 0),
  	      MG_BLACK, font, trackHubSkipHubName(database));
      }
  }
  
  static int doDrawRuler(struct hvGfx *hvg, int *rulerClickHeight,
                         int rulerHeight, int yAfterRuler, int yAfterBases, MgFont *font,
                         int fontHeight, boolean rulerCds, int scaleBarTotalHeight, struct window *window)
  /* draws the ruler. */
  {
  int titleHeight = fontHeight;
  int baseHeight = fontHeight;
  //int yAfterBases = yAfterRuler;
  int showPosHeight = fontHeight;
  int codonHeight = fontHeight;
  struct dnaSeq *seq = NULL;
  int rulerClickY = 0;
  *rulerClickHeight = rulerHeight;
  
  int y = rulerClickY;
  hvGfxSetClip(hvg, insideX, y, insideWidth, yAfterRuler-y+1);
  int relNumOff = winStart;
  
  if (baseTitle)
      {
      if (window == windows) // first window, only need to do once
  	{
  	hvGfxUnclip(hvg);
  	hvGfxSetClip(hvg, fullInsideX, y, fullInsideWidth, yAfterRuler-y+1);
  	hvGfxTextCentered(hvg, fullInsideX, y, fullInsideWidth, titleHeight,MG_BLACK, font, baseTitle);
  	hvGfxUnclip(hvg);
  	hvGfxSetClip(hvg, insideX, y, insideWidth, yAfterRuler-y+1);
  	}
      *rulerClickHeight += titleHeight;
      y += titleHeight;
      }
  if (baseShowPos||baseShowAsm)
      {
      if (window == windows) // first window, only need to do once
  	{
  	hvGfxUnclip(hvg);
  	hvGfxSetClip(hvg, fullInsideX, y, fullInsideWidth, yAfterRuler-y+1);
  	char txt[256];
  	char numBuf[SMALLBUF];
  	char *freezeName = NULL;
  	freezeName = hFreezeFromDb(database);
  	sprintLongWithCommas(numBuf, virtWinEnd-virtWinStart);
  	if (freezeName == NULL)
  	    freezeName = cloneString("Unknown");
  	if (baseShowPos&&baseShowAsm)
  	    safef(txt,sizeof(txt),"%s %s   %s (%s bp)",trackHubSkipHubName(organism),
  		    freezeName, addCommasToPos(database, position), numBuf);
  	else if (baseShowPos)
  	    safef(txt,sizeof(txt),"%s (%s bp)",addCommasToPos(database, position),numBuf);
  	else
  	    safef(txt,sizeof(txt),"%s %s",trackHubSkipHubName(organism),freezeName);
  	freez(&freezeName);
  	hvGfxTextCentered(hvg, fullInsideX, y, fullInsideWidth, showPosHeight,MG_BLACK, font, txt);
  	hvGfxUnclip(hvg);
  	hvGfxSetClip(hvg, insideX, y, insideWidth, yAfterRuler-y+1);
  	}
      *rulerClickHeight += showPosHeight;
      y += showPosHeight;
      }
  if (baseShowScaleBar)
      {
      if (window == windows) // first window
  	{
  	hvGfxUnclip(hvg);
  	hvGfxSetClip(hvg, fullInsideX, y, fullInsideWidth, yAfterRuler-y+1);
  	drawScaleBar(hvg, font, fontHeight, yAfterRuler, y, scaleBarTotalHeight);
  	hvGfxUnclip(hvg);
  	hvGfxSetClip(hvg, insideX, y, insideWidth, yAfterRuler-y+1);
  	}
      y += scaleBarTotalHeight;
      *rulerClickHeight += scaleBarTotalHeight;
      }
  if (baseShowRuler && (insideWidth >=36))
      {
      hvGfxDrawRulerBumpText(hvg, insideX, y, rulerHeight, insideWidth, MG_BLACK,
                             font, relNumOff, winBaseCount, 0, 1);
      }
  
  if (zoomedToBaseLevel || rulerCds)
      {
      Color baseColor = MG_BLACK;
      int start, end, chromSize;
      struct dnaSeq *extraSeq;
      /* extraSeq has extra leading & trailing bases
      * for translation in to amino acids */
      boolean complementRulerBases = cartUsualBooleanDb(cart, database, COMPLEMENT_BASES_VAR, FALSE);
      // gray bases if not matching the direction of display
      if (complementRulerBases != revCmplDisp)
  	baseColor = MG_GRAY;
  
      /* get sequence, with leading & trailing 3 bases
       * used for amino acid translation */
      start = max(winStart - 3, 0);
      chromSize = hChromSize(database, chromName);
      end = min(winEnd + 3, chromSize);
      extraSeq = hDnaFromSeq(database, chromName, start, end, dnaUpper);
      if (start != winStart - 3 || end != winEnd + 3)
  	{
  	/* at chromosome boundaries, pad with N's to assure
  	 * leading & trailing 3 bases */
  	char header[4] = "NNN", trailer[4] = "NNN";
  	int size = winEnd - winStart + 6;
  	char *padded = (char *)needMem(size+1);
  	header[max(3 - winStart, 0)] = 0;
  	trailer[max(winEnd - chromSize + 3, 0)] = 0;
  	safef(padded, size+1, "%s%s%s", header, extraSeq->dna, trailer);
  	extraSeq = newDnaSeq(padded, strlen(padded), extraSeq->name);
  	}
  
      /* for drawing bases, must clip off leading and trailing 3 bases */
      seq = cloneDnaSeq(extraSeq);
      seq = newDnaSeq(seq->dna+3, seq->size-6, seq->name);
  
      if (zoomedToBaseLevel)
          drawBases(hvg, insideX, y+rulerHeight, insideWidth, baseHeight,
                    baseColor, font, complementRulerBases, seq);
  
      /* set up clickable area to toggle ruler visibility */
          {
          char newRulerVis[100];
  	safef(newRulerVis, 100, "%s=%s", RULER_TRACK_NAME,
                rulerMode == tvFull ? rulerMenu[tvDense] : rulerMenu[tvFull]);
  	// GALT TODO should this be fullInsideX and fullInsideWidth?
  	mapBoxReinvoke(hvg, insideX, y+rulerHeight, insideWidth,baseHeight, NULL,
  	    FALSE, NULL, 0, 0, "", newRulerVis);
  	}
      if (rulerCds)
  	{
  	/* display codons */
  	int frame;
  	int firstFrame = 0;
  	int mod;            // for determining frame ordering on display
  	struct simpleFeature *sfList;
  	double scale = scaleForWindow(insideWidth, winStart, winEnd);
  
  	/* WARNING: tricky code to assure that an amino acid
  	 * stays in the same frame line on the browser during panning.
  	 * There may be a simpler way... */
  	if (complementRulerBases)
  	    mod = (chromSize - winEnd) % 3;
  	else
  	    mod = winStart % 3;
  	if (mod == 0)
  	    firstFrame = 0;
  	else if (mod == 1)
  	    firstFrame = 2;
  	else if (mod == 2)
  	    firstFrame = 1;
  
  	y = yAfterBases;
  	if (complementRulerBases)
  	    reverseComplement(extraSeq->dna, extraSeq->size);
  	for (frame = 0; frame < 3; frame++, y += codonHeight)
  	    {
  	    /* reference frame to start of chromosome */
  	    int refFrame = (firstFrame + frame) % 3;
  
  	    /* create list of codons in the specified coding frame */
  	    sfList = baseColorCodonsFromDna(refFrame, winStart, winEnd,
                                              extraSeq, complementRulerBases);
  	    /* draw the codons in the list, with alternating colors */
  	    baseColorDrawRulerCodons(hvg, sfList, scale, insideX, y,
                                       codonHeight, font, winStart, MAXPIXELS,
                                       zoomedToCodonLevel);
  	    }
  	}
      }
  hvGfxUnclip(hvg);
  return y;
  }
  
  static void logTrackList(struct dyString *dy, struct track *trackList)
  /* add visibile tracks to dyString, recursively called */
  {
  if (trackList == NULL)
      return;
  
  struct track *track;
  for (track = trackList; track != NULL; track = track->next)
      {
      int vis = track->limitedVisSet ? track->limitedVis : track->visibility;
      if (vis)
  	{
  	logTrackList(dy, track->subtracks);
  	if (dy->stringSize)
  	    dyStringAppendC(dy, ',');
  	dyStringPrintf(dy,"%s:%d", track->track, vis);
  	}
      }
  }
  
  static void logTrackVisibilities (char *hgsid, struct track *trackList)
  /* log visibile tracks and hgsid */
  {
  struct dyString *dy = newDyString(1024);
  
  // build up dyString
  logTrackList(dy, trackList);
  
  // put out ~1024 bye blocks to error_log because otherwise
  // Apache will chop up the lines
  char *begin = dy->string;
  char *ptr = begin;
  int count = 0;
  for(ptr=begin; ((ptr = strchr(ptr, ',')) != NULL); ptr++)
      {
      if (ptr - begin > 800)
  	{
  	*ptr = 0;
  	fprintf(stderr, "trackLog %d %s %s %s\n", count++, database, hgsid, begin);
  	begin = ptr+1;
  	}
      }
  fprintf(stderr, "trackLog %d %s %s %s\n", count++, database, hgsid, begin);
  
  dyStringFree(&dy);
  }
  
  static void rAddToTrackHash(struct hash *trackHash, struct track *trackList)
  /* Add list and any children of list to hash. */
  {
  struct track *track;
  for (track = trackList; track != NULL; track = track->next)
      {
      hashAddUnique(trackHash, track->track, track);
      rAddToTrackHash(trackHash, track->subtracks);
      }
  }
  
  struct highlightVar
  // store highlight information
  {
  struct highlightVar *next;
  char *db;
  char *chrom;
  long chromStart;
  long chromEnd;
  char *hexColor;
  };
  
  struct highlightVar *parseHighlightInfo()
  // Parse highlight info from cart var to a linked list of highlightVar structs
  // db.chrom:start-end#hexColor|db.chrom:start-end#hexColor|...
  {
  struct highlightVar *hlList = NULL;
  char *highlightDef = cartOptionalString(cart, "highlight");
  if(highlightDef)
      {
      char *hlArr[256];
      int hlCount = chopByChar(cloneString(highlightDef), '|', hlArr, ArraySize(hlArr));
      int i;
      for (i=0; i<hlCount; i++)
          {
          char *oneHl = hlArr[i];
          struct highlightVar *h;
          AllocVar(h);
          h->db     = cloneNextWordByDelimiter(&oneHl,'.');
          h->chrom  = cloneNextWordByDelimiter(&oneHl,':');
          // long to handle virt chrom coordinates
          h->chromStart = atol(cloneNextWordByDelimiter(&oneHl,'-'));
          h->chromEnd   = atol(cloneNextWordByDelimiter(&oneHl,'#'));
          h->chromStart--; // Not zero based
          if (highlightDef && *highlightDef != '\0')
              h->hexColor = cloneString(oneHl);
          slAddHead(&hlList, h);
          }
      slReverse(&hlList);
      }
  return hlList;
  }
  
  static void highlightRegions(struct cart *cart, struct hvGfx *hvg, int imagePixelHeight)
  // Highlights regions in the image.  Only done if theImgBox is not defined.
  // Thus it is done for ps/pdf and view image but the hgTracks image is highlighted via js
  {
  struct highlightVar *hlList = parseHighlightInfo();
  
  if(hlList && theImgBox == NULL) // Only highlight region when imgBox is not used. (pdf and show-image)
      {
      struct highlightVar *h;
      for (h=hlList; h; h=h->next)
          {
          if (virtualSingleChrom()) // DISGUISE VMODE
              {
              if ((h->db && sameString(h->db, database))
              &&  (h->chrom && sameString(h->chrom,chromName)))
                  {
                  char position[1024];
                  safef(position, sizeof position, "%s:%ld-%ld", h->chrom, h->chromStart, h->chromEnd);
                  char *newPosition = undisguisePosition(position); // UN-DISGUISE VMODE
                  if (startsWith("virt:", newPosition))
                      {
                      parseVPosition(newPosition, &h->chrom, &h->chromStart, &h->chromEnd);
                      }
                  }
              }
  
          if ((h->db && sameString(h->db, database))
          &&  (h->chrom && sameString(h->chrom,virtChromName))
          &&  (h->chromEnd != 0)
          &&  (h->chromStart <= virtWinEnd && h->chromEnd >= virtWinStart))
              {
  
              h->chromStart = max(h->chromStart, virtWinStart);
              h->chromEnd = min(h->chromEnd, virtWinEnd);
              double pixelsPerBase = (double)fullInsideWidth/(virtWinEnd - virtWinStart);
              int startPixels = pixelsPerBase * (h->chromStart - virtWinStart); // floor
              if (startPixels < 0)
                  startPixels *= -1;  // reverse complement
              int width = pixelsPerBase * (double)(h->chromEnd - h->chromStart) + 0.5; // round up
              if (width < 2)
                  width = 2;
  
              // Default color to light blue, but if setting has color, use it.
              unsigned int hexColor = MAKECOLOR_32(170, 255, 255);
              if (h->hexColor)
                  {
                  long rgb = strtol(h->hexColor,NULL,16); // Big and little Endians
                  hexColor = MAKECOLOR_32( ((rgb>>16)&0xff), ((rgb>>8)&0xff), (rgb&0xff) );
                  }
  
              hvGfxBox(hvg, fullInsideX + startPixels, 0, width, imagePixelHeight, hexColor);
              }
          }
      }
  }
  
  struct hash *makeGlobalTrackHash(struct track *trackList)
  /* Create a global track hash and returns a pointer to it. */
  {
  trackHash = newHash(8);
  rAddToTrackHash(trackHash, trackList);
  return trackHash;
  }
  
  //void domAddMenu(char *afterMenuId, char *newMenuId, char *label)
  ///* Append a new drop down menu after a given menu, by changing the DOM with jquery  */
  //{
  //printf("$('#%s').last().after('<li class=\"menuparent\" id=\"%s\"><span>%s</span>"
  //    "<ul style=\"display: none; visibility: hidden;\"></ul></li>');\n",
  //    afterMenuId, newMenuId, label);
  //}
  //
  //void domAppendToMenu(char *menuId, char *url, char *label)
  ///* Add an entry to a drop down menu, by changing the DOM with jquery  */
  //{
  ////printf("$('#%s ul').last().after('<li><a target=\"_BLANK\" href=\"%s\">%s</a></li>');\n", menuId, url, label);
  //printf("$('#%s ul').append('<li><a target=\"_BLANK\" href=\"%s\">%s</a></li>');\n", menuId, url, label);
  //}
  
  //void menuBarAppendExtTools()
  ///* printf a little javascript that adds entries to a menu */
  //{
  //    char url[SMALLBUF];
  //    safef(url,ArraySize(url),"hgTracks?%s=%s&hgt.redirectTool=crispor",cartSessionVarName(), cartSessionId(cart));
  //    printf("<script>\n");
  //    printf("jQuery(document).ready( function() {\n");
  //    domAddMenu("view", "sendto", "Send to");
  //    domAppendToMenu("sendto", url, "Tefor CRISPR sites");
  //    printf("});\n");
  //    printf("</script>\n");
  //}
  
  
  char *rgbColorToString(struct rgbColor color)
  /* make rgbColor into printable string */
  {
  char buf[256];
  safef(buf, sizeof buf, "rgbColor r:%d g:%d b:%d", color.r, color.g, color.b);
  return cloneString(buf);
  }
  
  char *trackDumpString(struct track *track)
  /* Write out info on track to string */
  {
  struct dyString *dy = dyStringNew(256);
  
  dyStringPrintf(dy, "next: %lu\n", (unsigned long)track->next);
  //    struct track *next;   /* Next on list. */
  //
  dyStringPrintf(dy, "track: %s\n", track->track);
      //char *track;             /* Track symbolic name. Name on image map etc. Same as tdb->track. */
  dyStringPrintf(dy, "table: %s\n", track->table);
      //char *table;             /* Table symbolic name. Name of database table. Same as tdb->table.*/
  dyStringPrintf(dy, "visibility: %s\n", hStringFromTv(track->visibility));
      //enum trackVisibility visibility; /* How much of this want to see. */
  dyStringPrintf(dy, "limitedVis: %s\n", hStringFromTv(track->limitedVis));
      //enum trackVisibility limitedVis; /* How much of this actually see. */
  dyStringPrintf(dy, "limitedVisSet: %d\n", track->limitedVisSet);
      //boolean limitedVisSet;	     /* Is limited visibility set? */
  
  dyStringPrintf(dy, "longLabel: %s\n", track->longLabel);
      //char *longLabel;           /* Long label to put in center. */
  dyStringPrintf(dy, "shortLabel: %s\n", track->shortLabel);
      //char *shortLabel;          /* Short label to put on side. */
  
  dyStringPrintf(dy, "mapsSelf: %d\n", track->mapsSelf);
      //bool mapsSelf;          /* True if system doesn't need to do map box. */
  dyStringPrintf(dy, "drawName: %d\n", track->drawName);
      //bool drawName;          /* True if BED wants name drawn in box. */
  
  dyStringPrintf(dy, "colorShades: %lu\n", (unsigned long)track->colorShades);
      //Color *colorShades;	       /* Color scale (if any) to use. */
  dyStringPrintf(dy, "color: %s\n", rgbColorToString(track->color));
      //struct rgbColor color;     /* Main color. */
  dyStringPrintf(dy, "ixColor: %u\n", track->ixColor);
      //Color ixColor;             /* Index of main color. */
  dyStringPrintf(dy, "altColorShades: %lu\n", (unsigned long)track->altColorShades);
      //Color *altColorShades;     /* optional alternate color scale */
  dyStringPrintf(dy, "altColor: %s\n", rgbColorToString(track->altColor));
      //struct rgbColor altColor;  /* Secondary color. */
  dyStringPrintf(dy, "ixAltColor: %u\n", track->ixAltColor);
      //Color ixAltColor;
  
      //void (*loadItems)(struct track *tg);
      /* loadItems loads up items for the chromosome range indicated.   */
  
  dyStringPrintf(dy, "items: %lu\n", (unsigned long) track->items);
      //void *items;               /* Some type of slList of items. */
  
      //char *(*itemName)(struct track *tg, void *item);
      /* Return name of one of an item to display on left side. */
  
      //char *(*mapItemName)(struct track *tg, void *item);
      /* Return name to associate on map. */
  
      //int (*totalHeight)(struct track *tg, enum trackVisibility vis);
  	/* Return total height. Called before and after drawItems.
  	 * Must set the following variables. */
  dyStringPrintf(dy, "height: %d\n", track->height);
      //int height;                /* Total height - must be set by above call. */
  dyStringPrintf(dy, "lineHeight: %d\n", track->lineHeight);
      //int lineHeight;            /* Height per item line including border. */
  dyStringPrintf(dy, "heightPer: %d\n", track->heightPer);
      //int heightPer;             /* Height per item line minus border. */
  
      //int (*itemHeight)(struct track *tg, void *item);
      /* Return height of one item. */
  
      //int (*itemRightPixels)(struct track *tg, void *item);
      /* Return number of pixels needed to right of item for additional labeling. (Optional) */
  
      //void (*drawItems)(struct track *tg, int seqStart, int seqEnd,
  	//struct hvGfx *hvg, int xOff, int yOff, int width,
  	//MgFont *font, Color color, enum trackVisibility vis);
      /* Draw item list, one per track. */
  
      //void (*drawItemAt)(struct track *tg, void *item, struct hvGfx *hvg,
          //int xOff, int yOff, double scale,
  	//MgFont *font, Color color, enum trackVisibility vis);
      /* Draw a single option.  This is optional, but if it's here
       * then you can plug in genericDrawItems into the drawItems,
       * which takes care of all sorts of things including packing. */
  
      //int (*itemStart)(struct track *tg, void *item);
      /* Return start of item in base pairs. */
  
      //int (*itemEnd)(struct track *tg, void *item);
      /* Return end of item in base pairs. */
  
      //void (*freeItems)(struct track *tg);
      /* Free item list. */
  
      //Color (*itemColor)(struct track *tg, void *item, struct hvGfx *hvg);
      /* Get color of item (optional). */
  
      //Color (*itemNameColor)(struct track *tg, void *item, struct hvGfx *hvg);
      /* Get color for the item's name (optional). */
  
      //Color (*itemLabelColor)(struct track *tg, void *item, struct hvGfx *hvg);
      /* Get color for the item's label (optional). */
  
      //void (*mapItem)(struct track *tg, struct hvGfx *hvg, void *item,
                      //char *itemName, char *mapItemName, int start, int end,
                      //int x, int y, int width, int height);
      /* Write out image mapping for a given item */
  
  dyStringPrintf(dy, "hasUi: %d\n", track->hasUi);
      //boolean hasUi;	/* True if has an extended UI page. */
  dyStringPrintf(dy, "wigCartData: %lu\n", (unsigned long)track->wigCartData);
      //void *wigCartData;  /* pointer to wigCart */
  dyStringPrintf(dy, "extraUiData: %lu\n", (unsigned long)track->extraUiData);
      //void *extraUiData;	/* Pointer for track specific filter etc. data. */
  
      //void (*trackFilter)(struct track *tg);
      /* Stuff to handle user interface parts. */
  
  dyStringPrintf(dy, "customPt: %lu\n", (unsigned long)track->customPt);
      //void *customPt;  /* Misc pointer variable unique to track. */
  dyStringPrintf(dy, "customInt: %d\n", track->customInt);
      //int customInt;   /* Misc int variable unique to track. */
  dyStringPrintf(dy, "subType: %d\n", track->subType);
      //int subType;     /* Variable to say what subtype this is for similar tracks to share code. */
  
      /* Stuff for the various wig incarnations - sample, wig, bigWig */
  dyStringPrintf(dy, "minRange: %.2f, maxRange: %.2f\n", track->minRange, track->maxRange);
      //float minRange, maxRange;	  /*min and max range for sample tracks 0.0 to 1000.0*/
  dyStringPrintf(dy, "scaleRange: %.2f\n", track->scaleRange);
      //float scaleRange;             /* What to scale samples by to get logical 0-1 */
  dyStringPrintf(dy, "graphUpperLimit: %e, grapLowerLimit: %e\n", track->graphUpperLimit, track->graphLowerLimit);
      //double graphUpperLimit, graphLowerLimit;	/* Limits of actual data in window for wigs. */
  dyStringPrintf(dy, "preDrawContainer: %lu\n", (unsigned long)track->preDrawContainer);
      //struct preDrawContainer *preDrawContainer;  /* Numbers to graph in wig, one per pixel */
      //struct preDrawContainer *(*loadPreDraw)(struct track *tg, int seqStart, int seqEnd, int width);
  dyStringPrintf(dy, "wigGraphOutput: %lu\n", (unsigned long)track->wigGraphOutput);
      //struct wigGraphOutput *wigGraphOutput;  /* Where to draw wig - different for transparency */
      /* Do bits that load the predraw buffer.  Called to set preDrawContainer */
  
  dyStringPrintf(dy, "bbiFile: %lu\n", (unsigned long)track->bbiFile);
      //struct bbiFile *bbiFile;	/* Associated bbiFile for bigWig or bigBed. */
  
  dyStringPrintf(dy, "bedSize: %d\n", track->bedSize);
      //int bedSize;		/* Number of fields if a bed file. */
  dyStringPrintf(dy, "isBigBed: %d\n", track->isBigBed);
      //boolean isBigBed;		/* If a bed, is it a bigBed? */
  
  dyStringPrintf(dy, "isRemoteSql: %d\n", track->isRemoteSql);
      //boolean isRemoteSql;	/* Is using a remote mySQL connection. */
  dyStringPrintf(dy, "remoteSqlHost: %s\n", track->remoteSqlHost);
      //char *remoteSqlHost;	/* Host machine name for remote DB. */
  dyStringPrintf(dy, "remoteSqlUser: %s\n", track->remoteSqlUser);
      //char *remoteSqlUser;	/* User name for remote DB. */
  dyStringPrintf(dy, "remoteSqlPassword: %s\n", track->remoteSqlPassword);
      //char *remoteSqlPassword;	/* Password for remote DB. */
  dyStringPrintf(dy, "remoteSqlDatabase: %s\n", track->remoteSqlDatabase);
      //char *remoteSqlDatabase;	/* Database in remote DB. */
  dyStringPrintf(dy, "remoteSqlTable: %s\n", track->remoteSqlTable);
      //char *remoteSqlTable;	/* Table name in remote DB. */
  
  dyStringPrintf(dy, "otherDb: %s\n", track->otherDb);
      //char *otherDb;		/* Other database for an axt track. */
  
  dyStringPrintf(dy, "private: %d\n", track->private);
      //unsigned short private;	/* True(1) if private, false(0) otherwise. */
  dyStringPrintf(dy, "priority: %.2f\n", track->priority);
      //float priority;   /* Tracks are drawn in priority order. */
  dyStringPrintf(dy, "defaultPriority: %.2f\n", track->defaultPriority);
      //float defaultPriority;   /* Tracks are drawn in priority order. */
  dyStringPrintf(dy, "groupName: %s\n", track->groupName);
      //char *groupName;	/* Name of group if any. */
  dyStringPrintf(dy, "group: %lu\n", (unsigned long)track->group);
      //struct group *group;  /* Group this track is associated with. */
  dyStringPrintf(dy, "defaultGroupName: %s\n", track->defaultGroupName);
      //char *defaultGroupName;  /* default Group this track is associated with. */
  dyStringPrintf(dy, "canPack: %d\n", track->canPack);
      //boolean canPack;	/* Can we pack the display for this track? */
  dyStringPrintf(dy, "spaceSaver: %lu\n", (unsigned long)track->ss);
      //struct spaceSaver *ss;  /* Layout when packed. */
  
  dyStringPrintf(dy, "tdb: %lu\n", (unsigned long)track->tdb);
      //struct trackDb *tdb; /*todo:change visibility, etc. to use this */
  // ADDED by GALT
  if (track->tdb)
      dyStringPrintf(dy, "tdb settings:\n%s\n", track->tdb->settings);
  
  dyStringPrintf(dy, "expScale: %.2f\n", track->expScale);
      //float expScale;	/* What to scale expression tracks by. */
  dyStringPrintf(dy, "expTable: %s\n", track->expTable);
      //char *expTable;	/* Expression table in hgFixed. */
  
      // factorSource
  dyStringPrintf(dy, "sourceCount: %d\n", track->sourceCount);
      //int sourceCount;	/* Number of sources for factorSource tracks. */
  dyStringPrintf(dy, "sources: %lu\n", (unsigned long)track->sources);
      //struct expRecord **sources;  /* Array of sources */
  dyStringPrintf(dy, "sourceRightPixels: %d\n", track->sourceRightPixels);
      //int sourceRightPixels;	/* Number of pixels to right we'll need. */
  
      // exon/Next
  dyStringPrintf(dy, "exonArrows: %d\n", track->exonArrows);
      //boolean exonArrows;	/* Draw arrows on exons? */
  dyStringPrintf(dy, "exonArrowsAlways: %d\n", track->exonArrowsAlways);
      //boolean exonArrowsAlways;	/* Draw arrows on exons even with introns showing? */
  dyStringPrintf(dy, "nextExonButtonable: %d\n", track->nextExonButtonable);
      //boolean nextExonButtonable; /* Use the next-exon buttons? */
  dyStringPrintf(dy, "nextItemButtonable: %d\n", track->nextItemButtonable);
      //boolean nextItemButtonable; /* Use the next-gene buttons? */
  
  dyStringPrintf(dy, "itemAttrTbl: %lu\n", (unsigned long)track->itemAttrTbl);
      //struct itemAttrTbl *itemAttrTbl;  /* relational attributes for specific items (color) */
  
      /* fill in left label drawing area */
  dyStringPrintf(dy, "labelColor: %u\n", track->labelColor);
      //Color labelColor;   /* Fixed color for the track label (optional) */
  
      //void (*drawLeftLabels)(struct track *tg, int seqStart, int seqEnd,
      //                       struct hvGfx *hvg, int xOff, int yOff, int width, int height,
      //                       boolean withCenterLabels, MgFont *font,
      //                       Color color, enum trackVisibility vis);
  
  dyStringPrintf(dy, "subtracks: %lu\n", (unsigned long)track->subtracks);
      //struct track *subtracks;   /* list of subsidiary tracks that are
                                  //loaded and drawn by this track.  This
                                  //is used for "composite" tracks, such
                                  //as "mafWiggle */
  dyStringPrintf(dy, "parent: %lu\n", (unsigned long)track->parent);
      //struct track *parent;	/* Parent track if any */
  dyStringPrintf(dy, "prevTrack: %lu\n", (unsigned long)track->prevTrack);
      //struct track *prevTrack;    // if not NULL, points to track immediately above in the image.
                                  //    Needed by ConditionalCenterLabel logic
  
      //void (*nextPrevExon)(struct track *tg, struct hvGfx *hvg, void *item, int x, int y, int w, int h, boolean next);
      /* Function will draw the button on a track item and assign a map */
      /* box to it as well, so that a click will move the browser window */
      /* to the next (or previous if next==FALSE) item. This is meant to */
      /* jump to parts of an item already partially in the window but is */
      /* hanging off the edge... e.g. the next exon in a gene. */
  
      //void (*nextPrevItem)(struct track *tg, boolean next);
      /* If this function is given, it can dictate where the browser loads */
      /* up based on whether a next-item button on the longLabel line of */
      /* the track was pressed (as opposed to the next-item buttons on the */
      /* track items themselves... see nextPrevExon() ). This is meant for */
      /* going to the next/previous item currently unseen in the browser, */
      /* e.g. the next gene. SO FAR THIS IS UNIMPLEMENTED. */
  
      //char *(*itemDataName)(struct track *tg, char *itemName);
      /* If not NULL, function to translated an itemName into a data name.
       * This is can be used for looking up sequence, CDS, etc. It is used
       * to support item names that have uniqueness identifiers added to deal
       * with multiple alignments.  The resulting value should *not* be freed,
       * and it should be assumed that it might only remain valid for a short
       * period of time.*/
  
  dyStringPrintf(dy, "loadTime: %d\n", track->loadTime);
      //int loadTime;	/* Time it takes to load (for performance tuning) */
  dyStringPrintf(dy, "drawTime: %d\n", track->drawTime);
      //int drawTime;	/* Time it takes to draw (for performance tuning) */
  
  dyStringPrintf(dy, "remoteDataSource: %d\n", track->remoteDataSource);
      //enum enumBool remoteDataSource; /* The data for this track is from a remote source */
                     /* Slow retrieval means image can be rendered via an AJAX callback. */
  dyStringPrintf(dy, "customTrack: %d\n", track->customTrack);
      //boolean customTrack; /* Need to explicitly declare this is a custom track */
  dyStringPrintf(dy, "syncChildVisToSelf: %d\n", track->syncChildVisToSelf);
      //boolean syncChildVisToSelf;	/* If TRUE sync visibility to of children to self. */
  dyStringPrintf(dy, "networkErrMsg: %s\n", track->networkErrMsg);
      //char *networkErrMsg;        /* Network layer error message */
  dyStringPrintf(dy, "parallelLoading: %d\n", track->parallelLoading);
      //boolean parallelLoading;    /* If loading in parallel, usually network resources. */
  dyStringPrintf(dy, "summary: %lu\n", (unsigned long) track->summary);
      //struct bbiSummaryElement *summary;  /* for bigBed */
  dyStringPrintf(dy, "summAll: %lu\n", (unsigned long) track->sumAll);
      //struct bbiSummaryElement *sumAll;   /* for bigBed */
  dyStringPrintf(dy, "drawLabelInBox: %d\n", track->drawLabelInBox);
      //boolean drawLabelInBox;     /* draw labels into the features instead of next to them */
      //};
  
  return dyStringCannibalize(&dy);
  }
  
  char *makeDumpURL(char *text)
  /* Make a temp file to hold big dump. Return URL to it. */
  {
  // trackDump output is quite large
  struct tempName trkDmp;
  trashDirFile(&trkDmp, "hgt", "hgt", ".txt");
  FILE *f = mustOpen(trkDmp.forCgi, "w");
  fprintf(f, "%s", text);
  carefulClose(&f);
  return cloneString(trkDmp.forHtml);
  }
  
  char *makeTrackDumpLink(struct track *track)
  /* Make a track dump to trash, and return html link to it */
  {
  char *tds = trackDumpString(track);
  char *url = makeDumpURL(tds);
  char buf[1024];
  safef(buf, sizeof buf, "<A HREF=%s>URL</A>", url);
  freeMem(tds);
  freeMem(url);
  return cloneString(buf);
  }
  
  boolean regionsAreInOrder(struct virtRegion *virtRegion1, struct virtRegion *virtRegion2)
  /* Return TRUE if the regions are on the same chrom and non-overlapping
   * and are in-order, i.e. region 1 appears before region2. */
  {
  if (sameString(virtRegion1->chrom, virtRegion2->chrom) && virtRegion1->end <= virtRegion2->start)
      return TRUE;
  return FALSE;
  }
  
  /* --- Virtual Chromosome Functions --- */
  
  boolean virtualSingleChrom()
  /* Return TRUE if using virtual single chromosome mode */
  {
  return (sameString(virtModeType,"exonMostly") || sameString(virtModeType,"geneMostly"));
  }
  
  void parseVPosition(char *position, char **pChrom, long *pStart, long *pEnd)
  /* parse Virt position */
  {
  if (!position)
      {
      errAbort("position NULL");
      }
  char *vPos = cloneString(position);
  stripChar(vPos, ',');
  char *colon = strchr(vPos, ':');
  if (!colon)
      errAbort("position has no colon");
  char *dash = strchr(vPos, '-');
  if (!dash)
      errAbort("position has no dash");
  *colon = 0;
  *dash = 0;
  *pChrom = cloneString(vPos);
  *pStart = atol(colon+1) - 1;
  *pEnd = atol(dash+1);
  }
  
  void parseNVPosition(char *position, char **pChrom, int *pStart, int *pEnd)
  /* parse NonVirt position */
  {
  if (!position)
      {
      errAbort("position NULL");
      }
  char *vPos = cloneString(position);
  stripChar(vPos, ',');
  char *colon = strchr(vPos, ':');
  if (!colon)
      errAbort("position has no colon");
  char *dash = strchr(vPos, '-');
  if (!dash)
      errAbort("position has no dash");
  *colon = 0;
  *dash = 0;
  *pChrom = cloneString(vPos);
  *pStart = atoi(colon+1) - 1;
  *pEnd = atoi(dash+1);
  }
  
  char *disguisePositionVirtSingleChrom(char *position) // DISGUISE VMODE
  /* Hide the virt position, convert to real single chrom span.
   * position should be virt chrom span.
   * Can handle anything in the virt single chrom. */
  {
  /* parse Virt position */
  char *chrom = NULL;
  long start = 0;
  long end = 0;
  parseVPosition(position, &chrom, &start, &end);
  if (!sameString(chrom, "virt"))
      return position; // return original
  struct window *windows = makeWindowListFromVirtChrom(start, end);
  char *nonVirtChromName = windows->chromName;
  if (!sameString(nonVirtChromName, chromName))
      return position; // return original
  int nonVirtWinStart  = windows->winStart;
  int nonVirtWinEnd    = windows->winEnd;
  struct window *w;
  for (w=windows->next; w; w=w->next)
      {
      if (!sameString(w->chromName, nonVirtChromName))
  	return position; // return original
      if (w->winEnd < nonVirtWinEnd)
  	return position; // return original
      nonVirtWinEnd = w->winEnd;
      }
  char nvPos[256];
  safef(nvPos, sizeof nvPos, "%s:%d-%d", nonVirtChromName, nonVirtWinStart+1, nonVirtWinEnd);
  slFreeList(&windows);
  return cloneString(nvPos);
  }
  
  
  
  char *undisguisePosition(char *position) // UN-DISGUISE VMODE
  /* Find the virt position
   * position should be real chrom span.
   * Limitation: can only convert things in the current windows set. */
  {
  /* parse NonVirt position */
  char *chrom = NULL;
  int start = 0;
  int end = 0;
  parseNVPosition(position, &chrom, &start, &end);
  if (!sameString(chrom, chromName))
      return position; // return original
  long newStart = -1;
  long newEnd = -1;
  struct window *lastW = NULL;
  struct window *w = NULL;
  for (w = windows; w; w=w->next)
      {
      //  double check chrom is same thoughout all windows, otherwise warning, return original value
      if (!sameString(w->chromName, chromName))
  	{
  	return position; // return original
  	}
      // check that the regions are ascending and non-overlapping
      if (lastW && w->winStart < lastW->winEnd)
  	{
  	return position; // return original
  	}
      // overlap with position?
      //  if intersection,
      if (w->winEnd > start && end > w->winStart)
  	{
  	int s = max(start, w->winStart);
  	int e = min(end, w->winEnd);
  	long cs = s - w->winStart + w->virtStart;
  	long ce = e - w->winStart + w->virtStart;
  	if (newStart == -1)
  	    newStart = cs;
  	newEnd = ce;
  	}
      lastW = w;
     }
  if (newStart == -1) // none of the windows intersected with the position
      return position; // return original
  //  return new virt undisguised position as a string
  char newPos[1024];
  safef (newPos, sizeof newPos, "virt:%ld-%ld", (newStart+1), newEnd);
  return cloneString(newPos);
  }
  
  
  char *windowsSpanPosition()
  /* Return a position string that spans all the windows.
   * Windows should be on same chrom and ascending and non-overlapping.*/
  {
  char buf[256];
  char *chromName = windows->chromName;
  int start = windows->winStart;
  struct window *w = windows, *last = NULL;
  while(w->next) // find last window
      {
      last = w;
      w = w->next;
      if (!sameString(chromName, w->chromName))
  	errAbort("windowsSpanPosition: expected all windows to be on the same chrom but found %s and %s", chromName, w->chromName);
      if (w->winStart < last->winEnd)
  	errAbort("windowsSpanPosition: expected all windows to be ascending non-overlapping, found %d < %d", w->winStart, last->winEnd);
      }
  int end = w->winEnd;
  safef(buf, sizeof buf, "%s:%d-%d", chromName, start+1, end);
  return cloneString(buf);
  }
  
  void padVirtRegions(int windowPadding)
  /* Pad virt regions with windowPadding bases
   *
   * NOTE a simple padding would not worry about merging or order. Just expand the beginning and end of each region.
  
   * NOTE this assumes that the regions are in order, but tolerates hiccups in order.
  
   * DONE make it handle multiple chromosomes
   *
   * TODO what about just modifying the original list directly?
  
   * I do not know if this is handling merging correctly.
   * DONE Maybe I should just add the padding directly into the exon-fetch-merge code.
   * I have looked at that earlier, and it should work easily.
   * It might also have the advantage of not having to create a duplicate list?
   *
   * TODO how do I test that the output is correct.
   *  if the input has ordered non-duplicate regions, then the output should be likewise.
   * */
  {
  int regionCount = 0;
  long regionBases=0;
  struct virtRegion *virtRegion, *lastVirtRegion = NULL;
  int  leftWindowPadding = 0;
  int rightWindowPadding = 0;
  struct virtRegion *v, *newList = NULL;
  char *lastChrom = NULL;
  int chromSize = -1;
  for(virtRegion=virtRegionList; virtRegion; virtRegion = virtRegion->next)
      {
      AllocVar(v);
      if (!sameOk(virtRegion->chrom, lastChrom))
  	{
  	chromSize = hChromSize(database, virtRegion->chrom);
  	}
      v->chrom = virtRegion->chrom; // TODO is cloning the string needed?
      leftWindowPadding = windowPadding;
      rightWindowPadding = windowPadding;
      if (lastVirtRegion && regionsAreInOrder(lastVirtRegion, virtRegion))
  	{
  	int distToPrevRegion = virtRegion->start - lastVirtRegion->end;
  	if (distToPrevRegion < (2*windowPadding))
  	    {
  	    leftWindowPadding = distToPrevRegion/2;
  	    }
  	}
      if (virtRegion->next && regionsAreInOrder(virtRegion, virtRegion->next))
  	{
  	int distToNextRegion = virtRegion->next->start - virtRegion->end;
  	if (distToNextRegion < (2*windowPadding))
  	    {
  	    rightWindowPadding = (distToNextRegion+1)/2;
  	    // +1 to balance for odd number of bases between, arbitrarily adding it to right side
  	    }
  	}
      v->start = virtRegion->start - leftWindowPadding;
      v->end = virtRegion->end + rightWindowPadding;
      if (v->start < 0)
  	v->start = 0;
      if (v->end >= chromSize)
  	v->end = chromSize;
      regionBases += (v->end - v->start);
      slAddHead(&newList, v);
  
      lastVirtRegion = virtRegion;
      lastChrom = virtRegion->chrom;
      ++regionCount;
      }
  slReverse(&newList);
  virtRegionList = newList; // update new list --
  // TODO should the old one be freed? if so, the chrom name should use cloneString
  }
  
  
  void makeVirtChrom()
  /* build virtual chrom array from virt region list */
  {
  virtRegionCount = slCount(virtRegionList);
  AllocArray(virtChrom, virtRegionCount);
  struct virtRegion *v;
  int i = 0;
  long totalBases = 0;
  for(v=virtRegionList;v;v=v->next,++i)
      {
      virtChrom[i].virtPos = totalBases;
      virtChrom[i].virtRegion = v;
      totalBases += (v->end - v->start);
      }
  virtSeqBaseCount = totalBases;
  }
  
  void virtChromBinarySearch(long target, long *resultIndex, int *resultOffset)
  /* Do a binary search for the target position in the virtual chrom.
   * Return virtChrom Index and Offset if found.
   * Return -1 if target out of range. TODO maybe change to errAbort*/
  {
  //The binary search will either return match or out-of-range.
  long index = -1;
  int offset = -1;
  long a = 0; // start of the array
  long b = virtRegionCount - 1; // end of array where N = count of regions
  if (target >=0 && target < virtSeqBaseCount)
      {
      while(1)
  	{
  	long c = (a + b) / 2;
  
  	if (a > b)
  	    {
  	    index = b;
  	    break;
  	    }
  	else if (target == virtChrom[c].virtPos)
  	    {
  	    // (exact match)
  	    index = c;
  	    break;
  	    }
  	else if (target > virtChrom[c].virtPos)
  	    {
  	    a = c + 1;
  	    }
  	else if (target < virtChrom[c].virtPos)
  	    {
  	    b = c - 1;
  	    }
  	else
  	    {
  	    // probably should not happen
  	    errAbort("Unexpected outcome in virtChromBinarySearch.");
  	    }
  	}
      }
  else if (target == virtSeqBaseCount)
      { // tolerate this special case
      index = virtRegionCount - 1;
      }
  if (index >= 0)
      offset = target - virtChrom[index].virtPos;
  *resultIndex = index;
  *resultOffset = offset;
  }
  
  void testVirtChromBinarySearch()
  /* test virt chrom binary search */
  {
  warn("about to test virt chrom binary search: virtSeqBaseCount=%ld virtRegionCount=%d", virtSeqBaseCount, virtRegionCount);
  long i;
  int o;
  long target = -1;
  virtChromBinarySearch(target, &i, &o);
  if (i != -1)
      errAbort("target %ld result %ld %d ", target, i, o);
  for(target=0; target < virtSeqBaseCount; ++target)
      {
      virtChromBinarySearch(target, &i, &o);
      if (virtChrom[i].virtPos + o != target)
  	errAbort("target = %ld result %ld %d ", target, i, o);
      struct virtRegion *v = virtChrom[i].virtRegion;
      int size = v->end - v->start;
      if (o > size)
  	errAbort("target = %ld result %ld %d > size=%d", target, i, o, size);
      }
  target = virtSeqBaseCount;
  virtChromBinarySearch(target, &i, &o);
  if (i != virtRegionCount - 1)
      errAbort("target = virtSeqBaseCount = %ld result %ld %d ", target, i, o);
  target = virtSeqBaseCount+1;
  virtChromBinarySearch(target, &i, &o);
  if (i != -1)
      errAbort("target = virtSeqBaseCount = %ld result %ld %d ", target, i, o);
  warn("Got past test virt chrom binary search");
  }
  
  struct window *makeWindowListFromVirtChrom(long virtWinStart, long virtWinEnd)
  /* make list of windows from virtual position on virtualChrom */
  {
  // quick check of virt win start and end
  if (virtWinEnd == virtWinStart)
      return NULL;
  if (virtWinStart < 0 || virtWinStart >= virtSeqBaseCount)
      errAbort("unexpected error. virtWinStart=%ld out of range. virtSeqBaseCount=%ld", virtWinStart, virtSeqBaseCount);
  if (virtWinEnd < 0 || virtWinEnd > virtSeqBaseCount)
      errAbort("unexpected error. virtWinEnd=%ld out of range. virtSeqBaseCount=%ld", virtWinEnd, virtSeqBaseCount);
  if (virtWinEnd - virtWinStart < 1)
      errAbort("unexpected error. virtual window size < 1 base. virtWinStart=%ld virtWinEnd=%ld virtSeqBaseCount=%ld", virtWinStart, virtWinEnd, virtSeqBaseCount);
  long virtIndexStart;
  int  virtOffsetStart;
  virtChromBinarySearch(virtWinStart, &virtIndexStart, &virtOffsetStart);
  if (virtIndexStart == -1)
      errAbort("unexpected failure to find target virtWinStart %ld in virtChrom Array", virtWinStart);
  
  long virtIndexEnd;
  int  virtOffsetEnd;
  virtChromBinarySearch(virtWinEnd, &virtIndexEnd, &virtOffsetEnd);
  if (virtIndexEnd == -1)
      errAbort("unexpected failure to find target virtWinEnd %ld in virtChrom Array", virtWinEnd);
  
  // create new windows list from virt chrom
  struct window *windows = NULL;
  long i = virtIndexStart;
  int winCount = 0;
  long basesInWindows = 0; // TODO not actually using this variable
  for(i=virtIndexStart; i <= virtIndexEnd; ++i)
      {
      struct window *w;
      AllocVar(w);
      w->organism = organism; // obsolete
      w->database = database; // obsolete
      struct virtRegion *v = virtChrom[i].virtRegion;
      long virtPos = virtChrom[i].virtPos;
      w->chromName = v->chrom;
      if (i == virtIndexStart)
  	{
  	w->winStart = v->start + virtOffsetStart;
  	w->virtStart = virtPos + virtOffsetStart;
  	}
      else
  	{
  	w->winStart = v->start;
  	w->virtStart = virtPos;
  	}
      if (i == virtIndexEnd)
  	{
  	if (virtOffsetEnd == 0)
  	    continue;
  	w->winEnd = v->start + virtOffsetEnd;
  	}
      else
  	{
  	w->winEnd = v->end;
  	}
      w->virtEnd = w->virtStart + (w->winEnd - w->winStart);
      w->regionOdd = i % 2;
      basesInWindows += (w->winEnd - w->winStart);
      slAddHead(&windows, w);
      ++winCount;
      }
  slReverse(&windows);
  if (basesInWindows != (virtWinEnd-virtWinStart)) // was virtWinBaseCount but now I call this routine for highlight pos as well as virt pos.
      errAbort("makeWindowListFromVirtChrom: unexpected error basesInWindows(%ld) != virtWinBaseCount(%ld)", basesInWindows, virtWinEnd-virtWinStart);
  return windows;
  }
  
  char *nonVirtPositionFromWindows()
  /* Must have created the virtual chrom first.
   * Currently just a hack to use windows,
   * use the (first) window(s) from that to get real chrom name start end */
  {
  // assumes makeWindowListFromVirtChrom() has already been called.
  if (!windows)
      errAbort("nonVirtPositionFromWindows() unexpected error, windows list not initialized yet");
  char *nonVirtChromName = windows->chromName;
  int nonVirtWinStart  = windows->winStart;
  int nonVirtWinEnd    = windows->winEnd;
  // Extending this through more of the windows,
  // if it is on the same chrom and maybe not too far separated.
  struct window *w;
  for (w=windows->next; w; w=w->next)
      {
      if (sameString(w->chromName, nonVirtChromName) && w->winEnd > nonVirtWinEnd)
  	nonVirtWinEnd = w->winEnd;
      }
  // TODO Also consider preserving the original bases in windows width (with clipping).
  char nvPos[256];
  safef(nvPos, sizeof nvPos, "%s:%d-%d", nonVirtChromName, nonVirtWinStart+1, nonVirtWinEnd);
  return cloneString(nvPos);
  }
  
  char *nonVirtPositionFromHighlightPos()
  /* Must have created the virtual chrom first.
   * Currently just a hack to use windows,
   * use the (first) window(s) from that to get real chrom name start end */
  {
  struct highlightVar *h = parseHighlightInfo();
  
  if (!(h && h->db && sameString(h->db, database) && sameString(h->chrom,"virt")))
      return NULL;
  
  struct window *windows = makeWindowListFromVirtChrom(h->chromStart, h->chromEnd);
  
  char *nonVirtChromName = windows->chromName;
  int nonVirtWinStart  = windows->winStart;
  int nonVirtWinEnd    = windows->winEnd;
  // Extending this through more of the windows,
  // if it is on the same chrom and maybe not too far separated.
  struct window *w;
  for (w=windows->next; w; w=w->next)
      {
      if (sameString(w->chromName, nonVirtChromName) && w->winEnd > nonVirtWinEnd)
  	nonVirtWinEnd = w->winEnd;
      }
  // TODO Also consider preserving the original bases in windows width (with clipping).
  char nvPos[256];
  safef(nvPos, sizeof nvPos, "%s.%s:%d-%d#%s", h->db, nonVirtChromName, nonVirtWinStart+1, nonVirtWinEnd, h->hexColor);
  return cloneString(nvPos);
  }
  
  void allocPixelsToWindows()
  /* Allocate pixels to windows, sets insideWidth and insideX
   *
   * TODO currently uses a strategy that places a window at a pixel location
   * directly, because of round-off and missing small windows this can occasionally
   * lead to gaps not covered by any pixel.  Consider replacing it with something
   * that tries not to leave many gaps -- but how to do it without distortion of some window sizes?
   * */
  {
  double pixelsPerBase = (double)fullInsideWidth / virtWinBaseCount;
  long basesUsed = 0;
  int windowsTooSmall = 0;
  struct window **pWindows = &windows;
  struct window *window;
  int winCount = slCount(windows);
  for(window=windows;window;window=window->next)
      {
      int basesInWindow = window->winEnd - window->winStart;
      int pixelsInWindow = 0.5 + (double)basesInWindow * pixelsPerBase; // should this round up ? + 0.5?
      window->insideWidth = pixelsInWindow;
      window->insideX = fullInsideX + basesUsed * pixelsPerBase;
      basesUsed += basesInWindow;
  
      if (pixelsInWindow < 1) // remove windows less than one pixel from the list
  	{
  	*pWindows = window->next;
  	--winCount;
  	++windowsTooSmall;
  	}
      else
  	{
  	pWindows = &window->next;
  	}
      }
  }
  
  struct positionMatch *virtChromSearchForPosition(char *chrom, int start, int end, boolean findNearest)
  /* Search the virtual chrom for the query chrom, start, end position
   *
   * TODO GALT: Intially this can be a simple brute-force search of the entire virtChrom array.
   *
   * However, this will need to be upgraded to using a rangeTree or similar structure
   * to rapidly return multiple regions that overlap the query position.
   * */
  {
  struct positionMatch *list=NULL, *p;
  int nearestRegion = -1;
  boolean nearestAfter = TRUE;
  int nearestDistance = INT_MAX;
  int i;
  struct virtRegion *v = NULL;
  long virtPos = 0;
  for(i=0; i < virtRegionCount; ++i)
      {
      v = virtChrom[i].virtRegion;
      virtPos = virtChrom[i].virtPos;
      if (sameString(v->chrom,chrom))
  	{ // TODO will we need to support finding closest misses too?
  	if (v->end > start && end > v->start) // overlap
  	    {
  	    int s = max(start, v->start);
  	    int e = min(end  , v->end  );
  	    AllocVar(p);
  	    p->virtStart = virtPos + (s - v->start);
  	    p->virtEnd   = virtPos + (e - v->start);
  	    slAddHead(&list, p);
  	    }
  	else if (findNearest && (!list))
  	    {
  	    int thisDist = v->start - start;
  	    boolean thisAfter = TRUE;
  	    if (thisDist < 0) // absolute value
  		{
  		thisDist = -thisDist;
  		thisAfter = FALSE;
  		}
  	    if (thisDist < nearestDistance)
  		{
  		nearestRegion = i;
  		nearestDistance = thisDist;
  		nearestAfter = thisAfter;
  		}
  	    }
  	}
      }
  if (findNearest && (!list) && nearestRegion != -1)
      {
      i = nearestRegion;
      v = virtChrom[i].virtRegion;
      virtPos = virtChrom[i].virtPos;
      AllocVar(p);
      if (nearestAfter)
  	{
      	p->virtStart = virtPos;
  	p->virtEnd   = p->virtStart + (end - start);
  	}
      else
  	{
      	p->virtEnd = virtPos + (v->end - v->start);
  	p->virtStart = p->virtEnd - (end - start);
  	}
      if (p->virtEnd > virtSeqBaseCount)
  	p->virtEnd = virtSeqBaseCount;
      slAddHead(&list, p);
      }
  slReverse(&list);
  return list;
  }
  
  int matchVPosCompare(const void *elem1, const void *elem2)
  /* compare elements based on vPos */
  {
  const struct positionMatch *a = *((struct positionMatch **)elem1);
  const struct positionMatch *b = *((struct positionMatch **)elem2);
  if (a->virtStart == b->virtStart)
      errAbort("matchVPosCompare error should not happen: 2 elements being sorted have the same virtStart=%ld", a->virtStart);
  else if (a->virtStart > b->virtStart)
      return 1;
  return -1;
  }
  
  void matchSortOnVPos(struct positionMatch **pList)
  /* Sort positions by virtPos
   * pList will be sorted by chrom, start, end but we want it ordered by vPos
   */
  {
  slSort(pList, matchVPosCompare);
  }
  
  
  struct positionMatch *matchMergeContiguousVPos(struct positionMatch *list)
  /* Merge contiguous matches spanning multiple touching windows */
  {
  struct positionMatch *newList = NULL;
  struct positionMatch *m;
  long lastStart = -1;
  long lastEnd = -1;
  struct positionMatch *lastM = NULL;
  boolean inMerge = FALSE;
  long mergeStart = -1;
  long mergeEnd = -1;
  if (!list)
      return NULL;
  for(m=list; 1; m=m->next)
  // special loop condition allows it to stop AFTER it goes thru the loop once as m=NULL.
  // this flushes out the last value.
      {
      if (inMerge)
  	{
  	if (m && m->virtStart == lastEnd)
  	    {
  	    // continue merging, do nothing.
  	    // maybe could be freeing a skipped node here.
  	    }
  	else
  	    {
  	    inMerge = FALSE;
  	    mergeEnd = lastEnd;
  	    // create new merged node and add it to new list
  	    struct positionMatch *n;
  	    AllocVar(n);
  	    n->virtStart = mergeStart;
  	    n->virtEnd   = mergeEnd;
  	    slAddHead(&newList, n);
  	    }
  	}
      else
  	{
  	if (m && m->virtStart == lastEnd)
  	    {
  	    inMerge = TRUE;
  	    mergeStart = lastStart;
  	    }
  	else if (lastM) // just transfer the last node unmodified to the new list.
  	    {
  	    slAddHead(&newList, lastM);
  	    }
  	}
      if (!m)
  	break;
      lastStart = m->virtStart;
      lastEnd = m->virtEnd;
      lastM = m;
      }
  slReverse(&newList);
  return newList;
  }
  
  
  // ----------------------------------
  
  
  void initVirtRegionsFromSOD1Hardwired()
  /* create a testing regionlist from SOD1 (uc002ypa.3) hardwired */
  {
  virtRegionList = NULL;
  char *chrom="chr21";
  //int exonStarts[] = {33031934,33036102,33038761,33039570,33040783};
  //int exonEnds[]   = {33032154,33036199,33038831,33039688,33041243};
  // BASE-level zoom in two 20 bp exons and their on-screen junction
  int exonStarts[] = {33032134,33036102};
  int exonEnds[]   = {33032154,33036122};
  int i;
  for(i=0; i<ArraySize(exonStarts); ++i)
      {
      struct virtRegion *v;
      AllocVar(v);
      v->chrom = cloneString(chrom);
      v->start = exonStarts[i];
      v->end   = exonEnds[i];
      v->strand[0] = '+';
      v->strand[1] = 0;
      slAddHead(&virtRegionList, v);
      }
  slReverse(&virtRegionList);
  }
  
  void initVirtRegionsFromEmGeneTable(char *ucscTranscriptId)
  /* create a testing regionlist from knownGene transcript with given id */
  {
  struct sqlConnection *conn = hAllocConn(database);
  virtRegionList = NULL;
  struct sqlResult *sr;
  char **row;
  int rowOffset = 0;
  if (hIsBinned(database, emGeneTable)) // skip first bin column if any
      ++rowOffset;
  char query[256];
  sqlSafef(query, sizeof(query), "select * from %s where name='%s'", emGeneTable, ucscTranscriptId);
  sr = sqlGetResult(conn, query);
  while ((row = sqlNextRow(sr)) != NULL)
      {
      struct genePred *gene = genePredLoad(row+rowOffset);
      int i;
      for(i=0; i< gene->exonCount; ++i)
  	{
  	struct virtRegion *v;
  	AllocVar(v);
  	v->chrom = cloneString(gene->chrom);
  	v->start = gene->exonStarts[i];
  	v->end   = gene->exonEnds[i];
  	v->strand[0] = gene->strand[0];
      	v->strand[1] = 0;
  	slAddHead(&virtRegionList, v);
  	}
      genePredFree(&gene);
      }
  sqlFreeResult(&sr);
  slReverse(&virtRegionList);
  hFreeConn(&conn);
  }
  
  boolean initSingleAltHaplotype(char *haplotypeId)
  /* create a testing regionlist from haplotype with given id */
  {
  struct sqlConnection *conn = hAllocConn(database);
  if (!virtRegionList) // this should already contain allchroms.
      errAbort("unexpected error in initSingleAltHaplotype: virtRegionList is NULL, should contain all chroms");
  struct virtRegion *after = virtRegionList;
  virtRegionList = NULL;
  struct sqlResult *sr;
  char **row;
  char *table = endsWith(haplotypeId, "_fix") ? "fixLocations" : "altLocations";
  if (! hTableExists(database, table))
      {
      warn("initSingleAltHaplotype: table '%s' not found in database %s, "
           "can't find %s", table, database, haplotypeId);
      return FALSE;
      }
  
  // where is the alt haplo placed?
  char query[256];
  sqlSafef(query, sizeof(query), "select chrom, chromStart, chromEnd from %s "
           "where name rlike '^%s(:[0-9-]+)?'", table, haplotypeId);
  sr = sqlGetResult(conn, query);
  row = sqlNextRow(sr);
  if (!row)
      {
      warn("no haplotype found for [%s] in %s", haplotypeId, table);
      return FALSE;
      }
  char *haploChrom = cloneString(row[0]);
  int haploStart = sqlUnsigned(row[1]);
  int haploEnd   = sqlUnsigned(row[2]);
  sqlFreeResult(&sr);
  // what is the size of the alt haplo?
  int haploSize = hChromSize(database, haplotypeId); // hopefully this will work
  
  // insert into list replacing original haploChrom record
  struct virtRegion *before = NULL;
  boolean found = FALSE;
  long offset = 0;
  struct virtRegion *v;
  while ((v=slPopHead(&after)))
      {
      if (sameString(haploChrom, v->chrom))
  	{
  	found = TRUE;
  	break;
  	}
      offset += (v->end - v->start);
      slAddHead(&before, v);
      }
  if (!found)
      {
      warn("initSingleAltHaplotype: chrom %s on which alt %s is placed is not found in all chroms list!", haploChrom, haplotypeId);
      return FALSE;
      }
  slReverse(&before);
  
  // for now, make virtchrom with just one chrom plus its haplo in the middle
  
  defaultVirtWinStart = 0;
  defaultVirtWinEnd = 0;;
  
  AllocVar(v);
  v->chrom = haploChrom;
  v->start = 0;
  v->end   = haploStart;
  defaultVirtWinStart = v->end - haploSize;
  if (defaultVirtWinStart < 0)
      defaultVirtWinStart = 0;
  defaultVirtWinEnd = v->end;
  slAddHead(&virtRegionList, v);
  
  
  AllocVar(v);
  v->chrom = haplotypeId;
  v->start = 0;
  v->end   = haploSize;
  defaultVirtWinEnd += haploSize;
  slAddHead(&virtRegionList, v);
  
  AllocVar(v);
  v->chrom = haploChrom;
  v->start = haploEnd;
  int chromSize = hChromSize(database, haploChrom); // size of the regular chrom
  v->end   = chromSize;
  defaultVirtWinEnd += haploSize;
  if (defaultVirtWinEnd > chromSize)
      defaultVirtWinEnd = chromSize;
  slAddHead(&virtRegionList, v);
  
  slReverse(&virtRegionList);
  hFreeConn(&conn);
  
  defaultVirtWinStart += offset;
  defaultVirtWinEnd += offset;
  
  // concatenate the 3 lists.
  virtRegionList = slCat(before,slCat(virtRegionList,after));
  
  return TRUE;
  }
  
  
  void initVirtRegionsFromKnownCanonicalGenes(char *table)
  // OBSOLETED by initVirtRegionsFromEMGeneTableExons()
  /* Create a regionlist from knownCanonical genes (not exons) */
  // I was not expecting it, but 20% of the KC genes overlap with others.
  // Some are cDNAs, some are non-coding RNAs, some just look like junk.
  // But I have to add special logic to accomodate them.
  // Currently I just merge until there is no more overlap,
  // and then I start a new region. So 30K regions should reduce to about 24K regions or less.
  {
  struct sqlConnection *conn = hAllocConn(database);
  virtRegionList = NULL;
  struct sqlResult *sr;
  char **row;
  char query[256];
  sqlSafef(query, sizeof(query), "select chrom, chromStart, chromEnd from %s where chrom not like '%%_hap_%%' and chrom not like '%%_random'", table);
  sr = sqlGetResult(conn, query);
  char chrom[256] = "";
  int start = -1;
  int end = -1;
  char lastChrom[256] = "";
  int lastStart = -1;
  int lastEnd = -1;
  boolean firstTime = TRUE;
  boolean isEOF = FALSE;
  while (1)
      {
      boolean printIt = FALSE;
      row = sqlNextRow(sr);
      if (row)
  	{
  	safecpy(chrom, sizeof chrom, row[0]);
  	start = sqlUnsigned(row[1]);
      	end   = sqlUnsigned(row[2]);
  	if (sameString(chrom, lastChrom))
  	    {
  	    if (start <= lastEnd)
  		{
  		// overlap detected in knownCanonical
  		// extend current region
  		if (end > lastEnd)
  		    lastEnd = end;
  		}
  	    else
  		{
  		printIt = TRUE;
  		}
  	    }
  	else
  	    {
  	    printIt = TRUE;
  	    }
  	}
      else
  	{
  	printIt = TRUE;
  	isEOF = TRUE;
  	}
  
  
      if (printIt)
  	{
  	if (firstTime)
  	    {
  	    firstTime = FALSE;
  	    }
  	else
  	    {
  	    struct virtRegion *v;
  	    AllocVar(v);
  	    v->chrom = cloneString(lastChrom);
  	    v->start = lastStart;
  	    v->end   = lastEnd;
  	    v->strand[0] = '.';  // TODO we should probably just remove the strand field
  	    v->strand[1] = 0;
  	    slAddHead(&virtRegionList, v);
  	    }
  	}
  
      if (isEOF)
  	break;
  
      if (printIt)
  	{
  	safecpy(lastChrom, sizeof lastChrom, chrom);
  	lastStart = start;
  	lastEnd = end;
  	}
  
  
      }
  sqlFreeResult(&sr);
  slReverse(&virtRegionList);
  hFreeConn(&conn);
  }
  
  struct kce
  // keep list of overlapping genes and their exons
  {
  struct kce *next;
  struct genePred *gene;
  int exonNumber;
  };
  
  int findBestKce(struct kce *list, struct kce **pBestKce, struct kce **pPrevKce)
  // find best kce by having minimum exon start
  // TODO could replace this with a heap or a doubly-linked list
  {
  int best = -1;
  struct kce *e, *prev = NULL;
  for(e=list; e; prev=e, e=e->next)
      {
      int start = e->gene->exonStarts[e->exonNumber];
      if ((start < best) || (best == -1))
  	{
  	best = start;
  	*pBestKce = e;
  	*pPrevKce = prev;
  	}
      }
  return best;
  }
  
  static void padExons(struct genePred *gene, int chromSize, int padding)
  /* pad all of the exons */
  {
  int i;
  for(i=0; i < gene->exonCount; ++i)
      {
      gene->exonStarts[i] -= padding;
      if (gene->exonStarts[i] < 0)
  	gene->exonStarts[i] = 0;
      gene->exonEnds[i] += padding;
      if (gene->exonEnds[i] > chromSize)
  	gene->exonEnds[i] = chromSize;
      }
  }
  
  static void convertGenePredGeneToExon(struct genePred *gene)
  /* convert gene into a gene with just one exon that spans the entire gene */
  {
  if (gene->exonCount < 1)
      errAbort("unexpected input in convertGenePredGeneToExon(), gene->exonCount=%d < 1", gene->exonCount);
  gene->exonEnds[0] = gene->exonEnds[gene->exonCount - 1];
  gene->exonCount = 1;
  }
  
  void initVirtRegionsFromEMGeneTableExons(boolean showNoncoding, char *knownCanonical, char *knownToTag, boolean geneMostly)
  /* Create a regionlist from knownGene exons. */
  // Merge exon regions that overlap.
  
  // DONE Jim indicated that he would prefer it to include all transcripts, not just knownCanonical.
  
  // DONE Jim also suggested that we might want to handle padding right here in this step.
  // After thinking about it, I do not think it would be very hard because we are merging already.
  // Basically, just take the record from the db table row, add padding to start and end,
  // and clip for chromosome size.
  
  // TODO If we keep it at full genome level (instead of single chrom), then there is an apparent
  // sorting issue because although they are sorted on disk, they are usually sorted by chrom alphabetically
  // so that chr11 (not chr2) comes after chr1.  Instead of trying to specify the sort order in the query,
  // which is slow, or trying to read one chrom at a time in the sorted order which is also slow, we can instead
  // just fetch them in their native order, and then create a duplicate array and copy the contents
  // to it in memory, one chunk per chrom, which would be very fast, but temporarily require duplicate vchrom array mem.
  // Not sure what to do about assemblies with many scaffolds.
  //
  // Adding support for extra options from Gencode hg38 so we can filter for
  // comprehensive, splice-variants, non-coding subsets.
  
  {
  struct sqlConnection *conn = hAllocConn(database);
  virtRegionList = NULL;
  struct sqlResult *sr;
  char **row;
  int rowOffset = 0;
  char query[256];
  int padding = emPadding;
  if (sameString(virtModeType, "geneMostly"))
      padding = gmPadding;
  // knownCanonical Hash
  struct hash *kcHash = NULL;
  if (knownCanonical) // filter out alt splicing variants
      {
      // load up hash of canonical transcriptIds
      sqlSafef(query, sizeof(query), "select transcript from %s"
  	//" where chrom not like '%%_hap_%%' and chrom not like '%%_random'"
  	, knownCanonical);
      if (virtualSingleChrom())
  	sqlSafefAppend(query, sizeof(query), " where chrom='%s'", chromName);
      kcHash = newHash(10);
      sr = sqlGetResult(conn, query);
      while ((row = sqlNextRow(sr)) != NULL)
  	{
  	hashAdd(kcHash, row[0], NULL);
  	}
      sqlFreeResult(&sr);
      }
  // knownToTag basic hash
  struct hash *ktHash = NULL;
  if (knownToTag) // filter out all but Basic
      {
      // load up hash of canonical transcriptIds
      sqlSafef(query, sizeof(query), "select name from %s where value='basic'", knownToTag);
      ktHash = newHash(10);
      sr = sqlGetResult(conn, query);
      while ((row = sqlNextRow(sr)) != NULL)
  	{
  	hashAdd(ktHash, row[0], NULL);
  	}
      sqlFreeResult(&sr);
      }
  setEMGeneTrack();
  if (!emGeneTable)
      errAbort("Unexpected error, emGeneTable=NULL in initVirtRegionsFromEMGeneTableExons");
  if (hIsBinned(database, emGeneTable)) // skip first bin column if any
      ++rowOffset;
  sqlSafef(query, sizeof(query), "select * from %s", emGeneTable);
  if (virtualSingleChrom())
      sqlSafefAppend(query, sizeof(query), " where chrom='%s'", chromName);
  // TODO GALT may have to change this to in-memory sorting?
  // refGene is out of order because of genbank continuous loading
  // also, using where chrom= causes it to use indexes which disturb order returned.
  sqlSafefAppend(query, sizeof(query), " order by chrom, txStart");
  sr = sqlGetResult(conn, query);
  
  char chrom[256] = "";
  int start = -1;
  int end = -1;
  char lastChrom[256] = "";
  int lastStart = -1;
  int lastEnd = -1;
  int chromSize = -1;
  char lastChromSizeChrom[256] = "";
  boolean firstTime = TRUE;
  boolean isEOF = FALSE;
  struct kce *kceList = NULL, *bestKce, *prevKce;
  struct genePred *gene = NULL;
  while (1)
      {
  
      while(1) // get input if possible
  	{
  	
  	boolean readIt = FALSE;
  	if (!gene)
  	    readIt = TRUE;
  	if (isEOF)
  	    readIt = FALSE;
  	if (readIt)
  	    {
  	    row = sqlNextRow(sr);
  	    if (row)
  		{
  		gene = genePredLoad(row+rowOffset);
  		if (geneMostly)
  		    convertGenePredGeneToExon(gene);
  		if (!sameString(lastChromSizeChrom, gene->chrom))
  		    {
  		    chromSize = hChromSize(database, gene->chrom);
  		    safecpy(lastChromSizeChrom, sizeof lastChromSizeChrom, gene->chrom);
  		    }
  		if (padding > 0)
  		    padExons(gene, chromSize, padding); // handle padding
  		}
  	    else
  		{
  		isEOF = TRUE;
  		}
  	    }
  	if (gene && !showNoncoding && (gene->cdsStart == gene->cdsEnd))
  	    {
  	    //skip non-coding gene
  	    genePredFree(&gene);
  	    }
  	if (gene && knownCanonical && !hashLookup(kcHash, gene->name))
  	    {
  	    //skip gene not in knownCanonical hash
  	    genePredFree(&gene);
  	    }
  	if (gene && knownToTag && !hashLookup(ktHash, gene->name))
  	    {
  	    // skip gene not in knownToTag Basic hash
  	    genePredFree(&gene);
  	    }
  	boolean transferIt = FALSE;
  	if (gene && !kceList)
  	    {
  	    transferIt = TRUE;
  	    }
  	else if (gene && kceList)
  	    {
  	    // TODO need to check the chrom equality first
  	    int best = findBestKce(kceList, &bestKce, &prevKce);
  	    if (sameString(gene->chrom, chrom))
  		{
  		if (gene->exonStarts[0] < best)
  		    transferIt = TRUE;
  		}
  	    }
  	if (transferIt)
  	    {
  	    // add gene to kce list
  	    struct kce *kce;
  	    AllocVar(kce);
  	    kce->gene = gene;
  	    kce->exonNumber = 0;
  	    slAddHead(&kceList, kce);
  	    safecpy(chrom, sizeof chrom, gene->chrom);
  	    gene = NULL; // do not free since it is still in use
  	    }
  	if (gene && kceList && !transferIt)
  	    break;
  	if (isEOF && !gene)
  	    {
  	    if (kceList) // flush out the last of the items in kcelist
  		findBestKce(kceList, &bestKce, &prevKce);
  	    break;
  	    }
  	}
  
  
      boolean printIt = FALSE;
  
      if (kceList)
  	{
  
  	safecpy(chrom, sizeof chrom, bestKce->gene->chrom);
  	start = bestKce->gene->exonStarts[bestKce->exonNumber];
  	end   = bestKce->gene->exonEnds[bestKce->exonNumber];
  
  	if (sameString(chrom, lastChrom))
  	    {
  	    if (start <= lastEnd)
  		{
  		// overlap detected, extend current region
  		if (end > lastEnd)
  		    {
  		    lastEnd = end;
  		    }
  		}
  	    else
  		{
  		printIt = TRUE;
  		}
  	    }
  	else
  	    {
  	    printIt = TRUE;
  	    }
  	}
      else
  	{
  	printIt = TRUE;
  	isEOF = TRUE;
  	}
  
      if (printIt)
  	{
  	if (firstTime)
  	    {
  	    firstTime = FALSE;
  	    }
  	else
  	    {
  	    struct virtRegion *v;
  	    AllocVar(v);
  	    v->chrom = cloneString(lastChrom);
  	    v->start = lastStart;
  	    v->end   = lastEnd;
  	    v->strand[0] = '.';  // TODO we should probably just remove the strand field
  	    v->strand[1] = 0;
  	    slAddHead(&virtRegionList, v);
  
  	    }
  	}
  
      if (isEOF && !kceList && !gene)
  	break;
  
      if (printIt)
  	{
  	safecpy(lastChrom, sizeof lastChrom, chrom);
  	lastStart = start;
  	lastEnd = end;
  	}
  
      ++bestKce->exonNumber;
      if (bestKce->exonNumber >= bestKce->gene->exonCount)
  	{  // remove from kceList
  	genePredFree(&bestKce->gene);
  	if (prevKce)
  	    prevKce->next = bestKce->next;
  	else
  	    kceList = bestKce->next;
  	freeMem(bestKce);
  	}
  
      }
  sqlFreeResult(&sr);
  slReverse(&virtRegionList);
  hashFree(&kcHash);
  hashFree(&ktHash);
  hFreeConn(&conn);
  }
  
  
  void testRegionList()
  /* check if it is ascending non-overlapping regions.
  (this is not always a requirement in the most general case, i.e. user-regions)
  */
  {
  char lastChrom[256];
  int lastEnd = -1;
  struct virtRegion *v;
  for (v=virtRegionList; v; v=v->next)
      {
      if (sameString(v->chrom,lastChrom))
  	{
  	if (v->end < v->start)
  	    errAbort("check of region list reveals invalid region %s:%d-%d", v->chrom, v->start, v->end);
  	if (lastEnd > v->start)
  	    errAbort("check of region list reveals overlapping regions region %s:%d-%d lastEnd=%d",  v->chrom, v->start, v->end, lastEnd);
  	}
      else
  	{
  	safecpy(lastChrom, sizeof lastChrom, v->chrom);
  	}
      lastEnd = v->end;
      }
  }
  
  
  // multi-window variables global to hgTracks
  
  
  void setGlobalsFromWindow(struct window *window)
  /* set global window values */
  {
  currentWindow = window;
  organism  = window->organism;
  database  = window->database;
  chromName = window->chromName;
  winStart  = window->winStart;
  winEnd    = window->winEnd;
  insideX   = window->insideX;
  insideWidth = window->insideWidth;
  winBaseCount = winEnd - winStart;
  }
  
  
  void initExonStep()
  /* create exon-like pattern with exonSize and stepSize */
  {
  int winCount = cartUsualInt(cart, "demo2NumWindows", demo2NumWindows);
  int i;
  int exonSize = cartUsualInt(cart, "demo2WindowSize", demo2WindowSize);  //200; //9974; //200;
  int intronSize = cartUsualInt(cart, "demo2StepSize", demo2StepSize); //200; //15000; // really using it like stepSize as that allows overlapping windows.
  struct virtRegion *v;
  for(i=0;i<winCount;++i)
      {
      AllocVar(v);
      //chr21:33,031,597-33,041,570
      v->chrom = "chr21";
      v->start = 33031597 - 1 + i*(intronSize);
      v->end = v->start + exonSize; //33041570;
      slAddHead(&virtRegionList, v);
      }
  slReverse(&virtRegionList);
  //if (winCount >= 2)
  //    withNextExonArrows = FALSE;	/* Display next exon navigation buttons near center labels? */
  //warn("winCount=%d, exonSize=%d, intronSize=%d", winCount, exonSize, intronSize);
  }
  
  void initAllChroms()
  /* initialize virt region list for main chromosomes */
  {
  struct sqlConnection *conn = hAllocConn(database);
  struct sqlResult *sr;
  char **row;
  int winCount = 0;
  char *query =
  NOSQLINJ "select chrom, size from chromInfo"
  " where chrom     like 'chr%'"
  " and   chrom not like '%_random'"
  " and   chrom not like 'chrUn%'";
  // allow alternate haplotypes for now
  //" and   chrom not like '%_hap%'"
  //" and   chrom not like '%_alt'"
  //warn("%s",query);
  struct virtRegion *v;
  sr = sqlGetResult(conn, query);
  while ((row = sqlNextRow(sr)) != NULL)
      {
      unsigned chromSize = sqlUnsigned(row[1]);
      AllocVar(v);
      v->chrom = cloneString(row[0]);
      v->start = 1 - 1;
      v->end   = chromSize;
      slAddHead(&virtRegionList, v);
      ++winCount;
      }
  sqlFreeResult(&sr);
  hFreeConn(&conn);
  slReverse(&virtRegionList);
  }
  
  
  
  void initWindowsAltLoci()
  /* initialize window list showing alt (alternate haplotype)*/
  {
  
  struct virtRegion *v;
  //chr1:153520530-153700530
  AllocVar(v);
  v->chrom = "chr1";
  v->start = 153520530 - 1;
  v->end   = 153700530;
  //chr1:153,520,529-154,045,739  as a single window
  slAddHead(&virtRegionList, v);
  
  //chr1_GL383518v1_alt:1-182439
  AllocVar(v);
  v->chrom = "chr1_GL383518v1_alt";
  v->start = 1 - 1;
  v->end   = 182439;
  slAddHead(&virtRegionList, v);
  
  //chr1:153865739-154045739
  AllocVar(v);
  v->chrom = "chr1";
  v->start = 153865739 - 1;
  v->end   = 154045739;
  slAddHead(&virtRegionList, v);
  
  slReverse(&virtRegionList);
  
  }
  
  void checkmultiRegionsBedInput()
  /* Check if multiRegionsBedInput needs processing.
   * If BED submitted, see if it has changed, and if so, save it to trash
   * and update cart and global vars. Uses sha1 hash for faster change check. */
  {
  enum custRgnType { empty, url, trashFile };
  enum custRgnType oldType = empty;
  enum custRgnType newType = empty;
  
  // OLD input
  
  char *newMultiRegionsBedUrl = NULL;
  multiRegionsBedUrl = cartUsualString(cart, "multiRegionsBedUrl", multiRegionsBedUrl);
  char multiRegionsBedUrlSha1Name[1024];
  safef(multiRegionsBedUrlSha1Name, sizeof multiRegionsBedUrlSha1Name, "%s.sha1", multiRegionsBedUrl);
  if (!multiRegionsBedUrl)
      {
      multiRegionsBedUrl = "";
      cartSetString(cart, "multiRegionsBedUrl", multiRegionsBedUrl);
      }
  if (sameString(multiRegionsBedUrl,""))
      oldType = empty;
  else if (strstr(multiRegionsBedUrl,"://"))
      oldType = url;
  else
      oldType = trashFile;
  
  // NEW input
  
  char *multiRegionsBedInput = cartOptionalString(cart, "multiRegionsBedInput");
  if (!multiRegionsBedInput)
      return;
  
  // create cleaned up dyString from input.
  // remove blank lines, trim leading and trailing spaces, change CRLF from TEXTAREA input to LF.
  struct dyString *dyInput = dyStringNew(1024);
  char *input = cloneString(multiRegionsBedInput);  // make a copy, linefile modifies
  struct lineFile *lf = lineFileOnString("multiRegionsBedInput", TRUE, input);
  char *line;
  int lineSize;
  while (lineFileNext(lf, &line, &lineSize))
      {
      line = trimSpaces(line);
      if (sameString(line, "")) // skip blank lines
  	continue;
      dyStringAppend(dyInput,line);
      dyStringAppend(dyInput,"\n");
      }
  lineFileClose(&lf);
  
  // test multiRegionsBedInput. empty? url? trashFile?
  input = cloneString(dyInput->string);  // make a copy, linefile modifies
  lf = lineFileOnString("multiRegionsBedInput", TRUE, input);
  int lineCount = 0;
  while (lineFileNext(lf, &line, &lineSize))
      {
      ++lineCount;
      if (lineCount==1 &&
  	(startsWithNoCase("http://" ,line)
        || startsWithNoCase("https://",line)
        || startsWithNoCase("ftp://"  ,line)))
  	{
  	// new value is a URL. set vars and cart.
  	newMultiRegionsBedUrl = cloneString(line);
  	newType = url;
  	}
      break;
      }
  lineFileClose(&lf);
  if (newType != url)
      {
      if (lineCount == 0)  // there are no non-blank lines
  	{	
  	newMultiRegionsBedUrl = "";
  	newType = empty;
  	}
      else
  	newType = trashFile;
      }
  
  char newSha1[(SHA_DIGEST_LENGTH + 1) * 2];
  if (newType==trashFile)
      {
      // calculate sha1 checksum on new input.
      unsigned char hash[SHA_DIGEST_LENGTH];
      SHA1((const unsigned char *)dyInput->string, dyInput->stringSize, hash);
      hexBinaryString(hash, SHA_DIGEST_LENGTH, newSha1, (SHA_DIGEST_LENGTH + 1) * 2);
      }
  
  // compare input sha1 to trashFile sha1 to see if same
  boolean filesAreSame = FALSE;
  if (oldType==trashFile && newType==trashFile)
      {
      lf = lineFileMayOpen(multiRegionsBedUrlSha1Name, TRUE);
      while (lineFileNext(lf, &line, &lineSize))
  	{
  	if (sameString(line, newSha1))
  	    filesAreSame = TRUE;
  	}
      lineFileClose(&lf);
      }
  
  // save new trashFile unless no changes.
  if (newType==trashFile && (!(oldType==trashFile && filesAreSame) ))
      {
      struct tempName bedTn;
      trashDirFile(&bedTn, "hgt", "custRgn", ".bed");
      FILE *f = mustOpen(bedTn.forCgi, "w");
      mustWrite(f, dyInput->string, dyInput->stringSize);
      carefulClose(&f);
      // new value is a trash file.
      newMultiRegionsBedUrl = cloneString(bedTn.forCgi);
      // save new input sha1 to trash file.
      safef(multiRegionsBedUrlSha1Name, sizeof multiRegionsBedUrlSha1Name, "%s.sha1", bedTn.forCgi);
      f = mustOpen(multiRegionsBedUrlSha1Name, "w");
      mustWrite(f, newSha1, strlen(newSha1));
      carefulClose(&f);
      }
  
  dyStringFree(&dyInput);
  
  // if new value, set vars and cart
  if (newMultiRegionsBedUrl)
      {
      multiRegionsBedUrl = newMultiRegionsBedUrl;
      cartSetString(cart, "multiRegionsBedUrl", multiRegionsBedUrl);
      }
  cartRemove(cart, "multiRegionsBedInput");
  }
  
  
  boolean initVirtRegionsFromBedUrl(time_t *bedDateTime)
  /* Read custom regions from BED URL */
  {
  multiRegionsBedUrl = cartUsualString(cart, "multiRegionsBedUrl", multiRegionsBedUrl);
  int bedPadding = 0; // default no padding
  if (sameString(multiRegionsBedUrl,""))
      {
      warn("No BED or BED URL specified.");
      return FALSE;
      }
  
  struct lineFile *lf = NULL;
  if (strstr(multiRegionsBedUrl,"://"))
      {
      lf = lineFileUdcMayOpen(multiRegionsBedUrl, FALSE);
      if (!lf)
  	{
  	warn("Unable to open [%s] with udc", multiRegionsBedUrl);
  	return FALSE;
  	}
      *bedDateTime = udcTimeFromCache(multiRegionsBedUrl, NULL);
      }
  else
      {
      lf = lineFileMayOpen(multiRegionsBedUrl, TRUE);
      if (!lf)
  	{
  	warn("BED custom regions file [%s] not found.", multiRegionsBedUrl);
  	return FALSE;
  	}
      *bedDateTime = 0;
      // touch corresponding .sha1 file to save it from trash cleaner.
      char multiRegionsBedUrlSha1Name[1024];
      safef(multiRegionsBedUrlSha1Name, sizeof multiRegionsBedUrlSha1Name, "%s.sha1", multiRegionsBedUrl);
      if (fileExists(multiRegionsBedUrlSha1Name))
  	readAndIgnore(multiRegionsBedUrlSha1Name);	
      }
  char *line;
  int lineSize;
  int expectedFieldCount = -1;
  struct bed *bed, *bedList = NULL;
  while (lineFileNext(lf, &line, &lineSize))
      {
      // Process comments for keywords like database, shortDesc, and maybe others
      if (startsWith("#",line))
  	{
  	if (startsWith("#database ",line))
  	    {
  	    char *dbFromBed = line+strlen("#database ");
  	    if (!sameString(database,dbFromBed))
  		{
  		warn("Multi-Region BED URL error: The database (%s) specified in input does not match current database %s",
  		   dbFromBed, database);
  		return FALSE;
  		}
  	    }
  	if (startsWith("#shortDesc ",line))
  	    {
  	    virtModeShortDescr = cloneString(line+strlen("#shortDesc "));
  	    }
  	if (startsWith("#padding ",line))
  	    {
  	    bedPadding = sqlSigned(line+strlen("#padding "));
  	    }
  	continue;
  	}
  
      char *row[15];
      int numFields = chopByWhite(line, row, ArraySize(row));
      if (numFields < 3)
  	{
          warn("%s doesn't appear to be in BED format. 3 or more fields required, got %d",
                  multiRegionsBedUrl, numFields);
  	return FALSE;
  	}
      if (expectedFieldCount == -1)
  	{
  	expectedFieldCount = numFields;
  	}
      else
  	{
  	if (numFields != expectedFieldCount)
  	    errAbort("Multi-Region BED was detected to have %d columns. But this row has %d columns. "
  	    "All rows except comment lines should have the same number of columns", numFields, expectedFieldCount);
  	}
  
      AllocVar(bed);
      // All fields are standard BED fields, no bedplus fields supported at this time.
      // note: this function does not validate chrom name or end beyond chrom size
      struct errCatch *errCatch = errCatchNew();
      if (errCatchStart(errCatch))
  	{
  	loadAndValidateBed(row, numFields, numFields+0, lf, bed, NULL, TRUE); // can errAbort
  	}
      errCatchEnd(errCatch);
      if (errCatch->gotError)
  	{
  	warn("%s", errCatch->message->string);
  	return FALSE;
  	}
      errCatchFree(&errCatch);
  
      bed->chrom=cloneString(bed->chrom);  // loadAndValidateBed does not do it for speed. but bedFree needs it.
  
      struct chromInfo *ci = hGetChromInfo(database, bed->chrom);
      if (ci == NULL)
  	{
          warn("Couldn't find chromosome/scaffold %s in database", bed->chrom);
  	return FALSE;
  	}
      if (bed->chromEnd > ci->size)
  	{
          warn("BED chromEnd %u > size %u for chromosome/scaffold %s", bed->chromEnd, ci->size, bed->chrom);
  	return FALSE;
  	}
  
      slAddHead(&bedList, bed);
  
      struct virtRegion *v;
      if (numFields < 12)
  	{
  	AllocVar(v);
  	v->chrom = cloneString(bed->chrom);
  	v->start = bed->chromStart;
  	v->end   = bed->chromEnd;
  	slAddHead(&virtRegionList, v);
  	}
      else
  	{
  	int e;
  	for (e = 0; e < bed->blockCount; ++e)
  	    {
  	    AllocVar(v);
  	    v->chrom = cloneString(bed->chrom);
  	    v->start = bed->chromStart + bed->chromStarts[e];
  	    v->end   = v->start + bed->blockSizes[e];
  	    slAddHead(&virtRegionList, v);
  	    }
  	}
  
      }
  lineFileClose(&lf);
  bedFreeList(&bedList);
  slReverse(&virtRegionList);
  if (bedPadding > 0)
      padVirtRegions(bedPadding);
  return TRUE;
  }
  
  void restoreSavedVirtPosition()
  /* Set state from lastDbPosCart.
   * This involves parsing the extra state that was saved.*/
  {
  
  struct hashEl *el, *elList = hashElListHash(lastDbPosCart->hash);
  for (el = elList; el != NULL; el = el->next)
      {
      char *cartVar = el->name;
      char *cartVal = cartOptionalString(lastDbPosCart, cartVar);
      if (cartVal)
  	{
  	/* do we need this feature?
  	if (sameString(cartVal,"(null)"))
  	    cartRemove(cart, cartVar);
  	else
          */	
  	cartSetString(cart, cartVar, cartVal);
  	}
      }
  hashElFreeList(&elList);
  
  }
  
  void lastDbPosSaveCartSetting(char *cartVar)
  /* Save var and value from cart into lastDbPosCart. */
  {
  cartSetString(lastDbPosCart, cartVar, cartUsualString(cart, cartVar, NULL));
  }
  
  void dySaveCartSetting(struct dyString *dy, char *cartVar, boolean saveBoth)
  /* Grab var and value from cart, save as var=val to dy string. */
  {
  if (dy->stringSize > 0)
      dyStringAppend(dy, " ");
  dyStringPrintf(dy, "%s=%s", cartVar, cartUsualString(cart, cartVar, NULL));
  if (saveBoth)
      lastDbPosSaveCartSetting(cartVar);
  }
  
  
  boolean initRegionList()
  /* initialize window list */
  {
  
  checkmultiRegionsBedInput();
  
  struct virtRegion *v;
  virtRegionList = NULL;
  virtModeExtraState = "";   // This is state that determines if the virtChrom has changed
  lastDbPosCart = cartOfNothing();  // USED to store and restore cart settings related to position and virtMode
  struct dyString *dy = dyStringNew(256);  // used to build virtModeExtraState
  
  if (sameString(virtModeType, "default"))
      {
      // Single window same as normal window
      // mostly good to test nothing was broken with single window
      AllocVar(v);
  
      v->chrom = chromName;
      v->start = 0;
      v->end = hChromSize(database, chromName);
  
      virtWinStart = winStart;
      virtWinEnd   = winEnd;
  
      slAddHead(&virtRegionList, v);
      slReverse(&virtRegionList);
      }
  else if (sameString(virtModeType, "exonMostly")
        || sameString(virtModeType, "geneMostly"))
      {
  
      // Gencode settings: comprehensive, alt-splice, non-coding
  
      char *knownCanonical = NULL;  // show splice-variants, not filtered out via knownCanonical
      boolean showNoncoding = TRUE; // show non-coding where cdsStart==cdsEnd
      char *knownToTag = NULL;      // show comprehensive set not filtered by knownToTag
      char varName[SMALLBUF];
      boolean geneMostly = FALSE;
  
      lastDbPosSaveCartSetting("emGeneTable");
  
      //DISGUISE makes obsolete dySaveCartSetting(dy, "emGeneTable");
      //DISGUISE makes obsolete dySaveCartSetting(dy, "emPadding");
      if (sameString(virtModeType, "geneMostly"))
  	geneMostly = TRUE;
      if (sameString(emGeneTable, "knownGene"))
  	{
  	// test cart var knownGene.show.noncoding
          // check for alternate table name.
          // if found, set and pass to gene-table reading routine
  
          // Some code borrowed from simpleTracks.c::loadKnownGene()
  
  	safef(varName, sizeof(varName), "%s.show.noncoding", emGeneTable);
  	showNoncoding = cartUsualBoolean(cart, varName, TRUE);
  	//DISGUISE makes obsolete dySaveCartSetting(dy, varName);
  	safef(varName, sizeof(varName), "%s.show.spliceVariants", emGeneTable);
  	boolean showSpliceVariants = cartUsualBoolean(cart, varName, TRUE);
  	//DISGUISE makes obsolete dySaveCartSetting(dy, varName);
  	if (!showSpliceVariants)
  	    {
  	    char *canonicalTable = trackDbSettingOrDefault(emGeneTrack->tdb, "canonicalTable", "knownCanonical");
  	    if (hTableExists(database, canonicalTable))
  		knownCanonical = canonicalTable;
  	    }
  	safef(varName, sizeof(varName), "%s.show.comprehensive", emGeneTable);
  	boolean showComprehensive = cartUsualBoolean(cart, varName, FALSE);
  	//DISGUISE makes obsolete dySaveCartSetting(dy, varName);
  	if (!showComprehensive)
  	    {
  	    if (hTableExists(database, "knownToTag"))
  		{
  		knownToTag = "knownToTag";
  		}
  	    }
  
  	}
      if (sameString(emGeneTable, "refGene"))
  	{
  	char varName[SMALLBUF];
  	safef(varName, sizeof(varName), "%s.hideNoncoding", emGeneTable);
  	showNoncoding = !cartUsualBoolean(cart, varName, FALSE);
  	//DISGUISE makes obsolete dySaveCartSetting(dy, varName);
  	}
  
      initVirtRegionsFromEMGeneTableExons(showNoncoding, knownCanonical, knownToTag, geneMostly);
      if (!virtRegionList)
  	{
  	warn("No genes found on chrom %s for track %s, returning to default view", chromName, emGeneTrack->shortLabel);
  	return FALSE;   // regionList is empty, nothing found.
  	}
      if (geneMostly)
  	virtModeShortDescr = "genes";
      else
  	virtModeShortDescr = "exons";
      // DISGUISE makes obsolete dyStringPrintf(dy," %s %s", dy->string, knownCanonical, knownToTag);
      }
  else if (sameString(virtModeType, "kcGenes")) // TODO obsolete
      {
      initVirtRegionsFromKnownCanonicalGenes("knownCanonical");
      virtModeShortDescr = "genes";
      }
  else if (sameString(virtModeType, "customUrl"))
      {
      // custom regions from BED URL
      virtModeShortDescr = "customUrl";  // can be overridden by comment in input bed file
      time_t bedDateTime = 0;
      if (!initVirtRegionsFromBedUrl(&bedDateTime))
  	{
  	return FALSE; // return to default mode
  	}
      dySaveCartSetting(dy, "multiRegionsBedUrl", TRUE);
      dyStringPrintf(dy, " %ld", (long)bedDateTime);
      }
  else if (sameString(virtModeType, "singleTrans"))
      {
      singleTransId = cartUsualString(cart, "singleTransId", singleTransId);
      if (sameString(singleTransId, ""))
  	{
  	warn("Single transcript Id should not be blank");
  	return FALSE; // return to default mode
  	}
      setEMGeneTrack();
      dySaveCartSetting(dy, "singleTransId", TRUE);
      }
  else if (sameString(virtModeType, "singleAltHaplo"))
      {
      singleAltHaploId = cartUsualString(cart, "singleAltHaploId", singleAltHaploId); // default is chr6_cox_hap2
      initAllChroms();  // we want to default to full genome view.
      if (!initSingleAltHaplotype(singleAltHaploId))
  	{
  	virtRegionList = NULL;
  	return FALSE; // return to default mode
  	}
      // was "single haplo" but that might confuse some users.
      virtModeShortDescr = endsWith(singleAltHaploId, "_fix") ? "fix patch" : "alt haplo";
      dySaveCartSetting(dy, "singleAltHaploId", TRUE);
      }
  else if (sameString(virtModeType, "allChroms"))
      { // TODO more work on this mode
      //warn("show all regular chromosomes (not alts)\n"
  	//"Warning must turn off all tracks except big*");
      initAllChroms();
      }
  else if (sameString(virtModeType, "demo1"))
      {
      // demo two windows on two chroms (default posn chr21, and same loc on chr22
  
      //chr21:33,031,597-33,041,570
      AllocVar(v);
      //chr21:33,031,597-33,041,570
      v->chrom = "chr21";
      v->start = 33031597 - 1;
      v->end = 33041570;
      slAddHead(&virtRegionList, v);
  
      struct virtRegion *v2;
  
      AllocVar(v2);
      //chr22:33,031,597-33,041,570
      v2->chrom = "chr22";
      v2->start = 33031597 - 1;
      v2->end = 33041570;
      slAddHead(&virtRegionList, v2);
  
      slReverse(&virtRegionList);
      }
  else if (sameString(virtModeType, "demo2"))
      {
      // demo multiple (70) windows on one chrom chr21 def posn, window size and step exon-like
      initExonStep();
      }
  else if (sameString(virtModeType, "demo4"))
      {
      // demo multiple (20) windows showing exons from TITIN gene uc031rqd.1.
      initVirtRegionsFromEmGeneTable("uc031rqd.1"); // TITIN // "uc002ypa.3"); // SOD1
      }
  else if (sameString(virtModeType, "demo5"))
      {
      // demo alt locus on hg38
      //  Shows alt chrom surrounded by preceding and following regions of same size from reference genome.
      initWindowsAltLoci();
      }
  else if (sameString(virtModeType, "demo6"))
      {
      // demo SOD1
      //  Shows zoomed in exon-exon junction from SOD1 gene, between exon1 and exon2.
  
      initVirtRegionsFromSOD1Hardwired();
  
      }
  else
      {
      // Unrecognized virtModeType
      return FALSE; // return to default mode
      }
  
  virtModeExtraState = dyStringCannibalize(&dy);
  
  if (!virtRegionList)
      return FALSE;   // regionList is empty, nothing found.
  
  return TRUE;
  
  }
  
  boolean isLimitedVisHiddenForAllWindows(struct track *track)
  /* Check if track limitedVis == hidden for all windows.
   * Return true if all are hidden */
  {
  boolean result = TRUE;
  for(;track;track=track->nextWindow)
      if (track->limitedVis != tvHide)
  	result = FALSE;
  return result;
  }
  
  boolean isTypeBedLike(struct track *track)
  /* Check if track type is BED-like packable thing (but not rmsk or joinedRmsk) */
  { // TODO GALT do we have all the types needed?
  // TODO could it be as simple as whether track->items exists?
  char *typeLine = track->tdb->type, *words[8], *type;
  int wordCount;
  if (typeLine == NULL)
      return FALSE;
  wordCount = chopLine(cloneString(typeLine), words);
  if (wordCount <= 0)
      return FALSE;
  type = words[0];
  // NOTE: if type is missing here, full mode fails to return an hgTracks object
  if (
  (  sameWord(type, "bed")
  || sameWord(type, "bed5FloatScore")
  || sameWord(type, "bed6FloatScore")
  || sameWord(type, "bedDetail")
  || sameWord(type, "bigBed")
  || sameWord(type, "bigGenePred")
  || sameWord(type, "broadPeak")
  || sameWord(type, "chain")
  || sameWord(type, "factorSource")
  || sameWord(type, "genePred")
  || sameWord(type, "gvf")
  || sameWord(type, "narrowPeak")
  || sameWord(type, "psl")
  || sameWord(type, "barChart")
  || sameWord(type, "bigBarChart")
  || sameWord(type, "interact")
  || sameWord(type, "bigInteract")
  || sameWord(type, "bigLolly")
  //|| track->loadItems == loadSimpleBed
  //|| track->bedSize >= 3 // should pick up several ENCODE BED-Plus types.
  )
  && track->canPack
     )
      {
      return TRUE;
      }
  
  return FALSE;
  }
  
  boolean isTypeUseItemNameAsKey(struct track *track)
- /* Check if track type is like expRatio and key is just item name. */
+ /* Check if track type is like expRatio and key is just item name, to link across multi regions */
  {
  char *typeLine = track->tdb->type, *words[8], *type;
  int wordCount;
  if (typeLine == NULL)
      return FALSE;
  wordCount = chopLine(cloneString(typeLine), words);
  if (wordCount <= 0)
      return FALSE;
  type = words[0];
  if (sameWord(type, "expRatio"))
      {
      // track is like expRatio, needs one row per item
      return TRUE;
      }
  return FALSE;
  }
  
+ boolean isTypeUseMapItemNameAsKey(struct track *track)
+ /* Check if track type is like interact and uses map item name to link across multi regions */
+ {
+ char *typeLine = track->tdb->type, *words[8], *type;
+ int wordCount;
+ if (typeLine == NULL)
+     return FALSE;
+ wordCount = chopLine(cloneString(typeLine), words);
+ if (wordCount <= 0)
+     return FALSE;
+ type = words[0];
+ if (sameWord(type, "interact") || sameWord(type, "bigInteract"))
+         return TRUE;
+ return FALSE;
+ }
+ 
  void setFlatTrackMaxHeight(struct flatTracks *flatTrack, int fontHeight)
  /* for each flatTrack, figure out maximum height needed from all windows */
  {
  struct track *track = flatTrack->track;
  int maxHeight = 0;
  struct track *winTrack;
  struct window *window;
  for (window=windows, winTrack=track; window; window=window->next, winTrack=winTrack->nextWindow)
      {
      setGlobalsFromWindow(window);
  
      int trackHeight =  trackPlusLabelHeight(winTrack, fontHeight);
  
      if (trackHeight > maxHeight)
  	maxHeight = trackHeight;
      }
  setGlobalsFromWindow(windows); // first window
  
  flatTrack->maxHeight = maxHeight;
  }
  
- char *collapseEmptySubtracksSetting(struct track *track)
 -boolean doCollapseEmptySubtracks(struct track *track)
 -/* Suppress display of empty subtracks. Initial support only for bed's. */
++char *hideEmptySubtracksSetting(struct track *track)
 +/* Setting to suppress display of empty subtracks */
  {
 +if (!tdbIsComposite(track->tdb))
 +    return FALSE;
- return trackDbSetting(track->tdb, "collapseEmptySubtracks");
+ char *collapse = trackDbSetting(track->tdb, "hideEmptySubtracks");
+ if (!collapse)
+     // previous syntax (not documented, but used by Regeneron)
+     collapse = trackDbSetting(track->tdb, "collapseEmptySubtracks");
 -return (collapse && (sameWord(collapse, "on") ||
 -                                        sameWord(collapse, "true")));
++return (collapse);
 +}
 +
- boolean doCollapseEmptySubtracksNoMultiBed(struct track *track)
- /* TRUE if collapseEmptySubtracks setting is 'on' (no file specs for multiBed */
++boolean doHideEmptySubtracksNoMultiBed(struct track *track)
++/* TRUE if hideEmptySubtracks setting is 'on'  or 'true' (no file specs for multiBed */
 +{
- char *setting = collapseEmptySubtracksSetting(track);
- if (setting && sameString(setting, "on"))
++char *setting = hideEmptySubtracksSetting(track);
++if (setting && (sameString(setting, "on") || sameString(setting, "true")))
 +    return TRUE;
 +return FALSE;
 +}
 +
- boolean doCollapseEmptySubtracks(struct track *track, char **multiBedFile, char **subtrackIdFile)
++boolean doHideEmptySubtracks(struct track *track, char **multiBedFile, char **subtrackIdFile)
 +{
 +/* Support setting to suppress display of empty subtracks. 
 + * (Initial support only for bed's).
 + * 
-  * Format:  collapseEmptySubtracks on
++ * Format:  hideEmptySubtracks on
 + *              or
-  *          collapseEmptySubtracks multiBed.bed subtrackIds.tab
++ *          hideEmptySubtracks multiBed.bed subtrackIds.tab
 + * where multiBed.bed is a bed3Sources bigBed, generated with bedtools multiinter
 + *              post-processed by UCSC multiBed.pl tool
 + *      subtrackIds.tab is a tab-sep file: id subtrackName
 + */
- char *collapse = collapseEmptySubtracksSetting(track);
++char *collapse = hideEmptySubtracksSetting(track);
 +if (!collapse)
 +    return FALSE;
- if (doCollapseEmptySubtracksNoMultiBed(track))
++if (doHideEmptySubtracksNoMultiBed(track))
 +    return TRUE;
 +char *orig = cloneString(collapse);
 +char *words[2];
 +int wordCount = chopByWhite(collapse, words, ArraySize(words));
 +if (wordCount == 2)
 +    {
 +    if (multiBedFile)
 +        *multiBedFile = cloneString(hReplaceGbdb(words[0]));
 +    if (subtrackIdFile)
 +        *subtrackIdFile = cloneString(words[1]);
 +    return TRUE;
 +    }
- warn("Track %s collapseEmptySubtracks setting invalid: %s",
++warn("Track %s hideEmptySubtracks setting invalid: %s",
 +                track->track, orig);
 +return FALSE;
 +}
 +
 +struct hash *getNonEmptySubtracks(struct track *track)
 +{
 +/* Support setting to suppress display of empty subtracks. 
 + * (Initial support only for bed's).
 + * 
-  * Format:  collapseEmptySubtracks on
++ * Format:  hideEmptySubtracks on
 + *              or
-  *          collapseEmptySubtracks multiBed.bed subtrackIds.tab
++ *          hideEmptySubtracks multiBed.bed subtrackIds.tab
 + * where multiBed.bed is a bed3Sources bigBed, generated with bedtools multiinter
 + *              post-processed by UCSC multiBed.pl tool
 + *      subtrackIds.tab is a tab-sep file: id subtrackName
 + *
 + * Return hash with subtrack names as keys
 + */
 +
 +char *multiBedFile = NULL;
 +char *subtracksIdFile = NULL;
- if (!doCollapseEmptySubtracks(track, &multiBedFile, &subtracksIdFile))
++if (!doHideEmptySubtracks(track, &multiBedFile, &subtracksIdFile))
 +    return NULL;
 +if (!multiBedFile)
 +    return NULL;
 +
 +// load multiBed items in window
 +// TODO: filters here ?
 +// TODO:  protect against temporary network error ? */
 +struct lm *lm = lmInit(0);
 +struct bbiFile *bbi = bigBedFileOpen(multiBedFile);
 +struct bigBedInterval *bb, *bbList =  bigBedIntervalQuery(bbi, chromName, winStart, winEnd, 0, lm);
 +char *row[bbi->fieldCount];
 +char startBuf[16], endBuf[16];
 +struct hash *nonEmptySubtracksHash = hashNew(0);
 +for (bb = bbList; bb != NULL; bb = bb->next)
 +    {
 +    bigBedIntervalToRow(bb, chromName, startBuf, endBuf, row, ArraySize(row));
 +    // TODO: do this in bed3Sources.c
 +    char *idList = row[4];
 +    struct slName *ids = slNameListFromComma(idList);
 +    struct slName *id = NULL;
 +    for (id = ids; id != NULL; id = id->next)
 +        hashStore(nonEmptySubtracksHash, id->name);
 +    // TODO: free some stuff ?
 +    }
 +
 +// read file containing ids of subtracks 
 +struct lineFile *lf = udcWrapShortLineFile(subtracksIdFile, NULL, 0); 
 +char *words[2];
 +while (lineFileChopNext(lf, words, sizeof words))
 +    {
 +    char *id = words[0];
 +    char *name = words[1];
 +    if (hashLookup(nonEmptySubtracksHash, id))
 +        {
 +        hashStore(nonEmptySubtracksHash, cloneString(name));
 +        }
 +    }
 +lineFileClose(&lf);
 +return nonEmptySubtracksHash;
++
  }
  
  void makeActiveImage(struct track *trackList, char *psOutput)
  /* Make image and image map. */
  {
  struct track *track;
  MgFont *font = tl.font;
  struct hvGfx *hvg;
  struct tempName pngTn;
  char *mapName = "map";
  int fontHeight = mgFontLineHeight(font);
  int trackPastTabX = (withLeftLabels ? trackTabWidth : 0);
  int trackTabX = gfxBorder;
  int trackPastTabWidth = tl.picWidth - trackPastTabX;
  int pixWidth, pixHeight;
  int y=0;
  int titleHeight = fontHeight;
  int scaleBarPad = 2;
  int scaleBarHeight = fontHeight;
  int scaleBarTotalHeight = fontHeight + 2 * scaleBarPad;
  int showPosHeight = fontHeight;
  int rulerHeight = fontHeight;
  int baseHeight = fontHeight;
  int basePositionHeight = rulerHeight;
  int codonHeight = fontHeight;
  int rulerTranslationHeight = codonHeight * 3;        // 3 frames
  int yAfterRuler = gfxBorder;
  int yAfterBases = yAfterRuler;  // differs if base-level translation shown
  boolean rulerCds = zoomedToCdsColorLevel;
  int rulerClickHeight = 0;
  int newWinWidth = 0;
  
  /* Figure out dimensions and allocate drawing space. */
  
  pixWidth = tl.picWidth;
  
  leftLabelX = gfxBorder;
  leftLabelWidth = insideX - gfxBorder*3;
  
  struct image *theOneImg  = NULL; // No need to be global, only the map needs to be global
  struct image *theSideImg = NULL; // Because dragScroll drags off end of image,
                                   //    the side label gets seen. Therefore we need 2 images!!
  //struct imgTrack *curImgTrack = NULL; // Make this global for now to avoid huge rewrite
  struct imgSlice *curSlice    = NULL; // No need to be global, only the map needs to be global
  //struct mapSet   *curMap      = NULL; // Make this global for now to avoid huge rewrite
  // Set up imgBox dimensions
  int sliceWidth[stMaxSliceTypes]; // Just being explicit
  int sliceOffsetX[stMaxSliceTypes];
  int sliceHeight        = 0;
  int sliceOffsetY       = 0;
  char *rulerTtl = NULL;
  
  if (theImgBox)
  // if theImgBox == NULL then we are rendering a simple single image such as right-click View image.
  // theImgBox is a global for now to avoid huge rewrite of hgTracks.  It is started
  // prior to this in doTrackForm()
      {
      rulerTtl = "drag select or click to zoom";
      hPrintf("<input type='hidden' name='db' value='%s'>\n", database);
      hPrintf("<input type='hidden' name='c' value='%s'>\n", chromName);
      hPrintf("<input type='hidden' name='l' value='%d'>\n", winStart);
      hPrintf("<input type='hidden' name='r' value='%d'>\n", winEnd);
      hPrintf("<input type='hidden' name='pix' value='%d'>\n", tl.picWidth);
      // If a portal was established, then set the global dimensions to the entire expanded image size
      if (imgBoxPortalDimensions(theImgBox,&virtWinStart,&virtWinEnd,&(tl.picWidth),NULL,NULL,NULL,NULL,NULL))
          {
          pixWidth = tl.picWidth;
          virtWinBaseCount = virtWinEnd - virtWinStart;
          fullInsideWidth = tl.picWidth - gfxBorder - fullInsideX;
          }
      memset((char *)sliceWidth,  0,sizeof(sliceWidth));
      memset((char *)sliceOffsetX,0,sizeof(sliceOffsetX));
      if (withLeftLabels)
          {
          sliceWidth[stButton]   = trackTabWidth + 1;
          sliceWidth[stSide]     = leftLabelWidth - sliceWidth[stButton] + 1;
          sliceOffsetX[stSide]   =
                            (revCmplDisp ? (tl.picWidth - sliceWidth[stSide] - sliceWidth[stButton])
                                         : sliceWidth[stButton]);
          sliceOffsetX[stButton] = (revCmplDisp ? (tl.picWidth - sliceWidth[stButton]) : 0);
          }
      sliceOffsetX[stData] = (revCmplDisp ? 0 : sliceWidth[stSide] + sliceWidth[stButton]);
      sliceWidth[stData]   = tl.picWidth - (sliceWidth[stSide] + sliceWidth[stButton]);
      }
  struct flatTracks *flatTracks = NULL;
  struct flatTracks *flatTrack = NULL;
  
  if (rulerMode != tvFull)
      {
      rulerCds = FALSE;
      }
  
  /* Figure out height of each visible track. */
  pixHeight = gfxBorder;
  
  // figure out height of ruler
  if (rulerMode != tvHide)
      {
      if (!baseShowRuler && !baseTitle && !baseShowPos && !baseShowAsm && !baseShowScaleBar && !zoomedToBaseLevel && !rulerCds)
          {
          warn("Can't turn everything off in base position track.  Turning ruler back on");
          baseShowRuler = TRUE;
          cartSetBoolean(cart, BASE_SHOWRULER, TRUE);
          }
  
      if (baseTitle)
          basePositionHeight += titleHeight;
  
      if (baseShowPos||baseShowAsm)
          basePositionHeight += showPosHeight;
  
      if (baseShowScaleBar)
          basePositionHeight += scaleBarTotalHeight;
  
      if (!baseShowRuler)
          {
          basePositionHeight -= rulerHeight;
          rulerHeight = 0;
          }
  
      if (zoomedToBaseLevel)
          basePositionHeight += baseHeight;
  
      yAfterRuler += basePositionHeight;
      yAfterBases = yAfterRuler;
      pixHeight += basePositionHeight;
      if (rulerCds)
          {
          yAfterRuler += rulerTranslationHeight;
          pixHeight += rulerTranslationHeight;
          }
      }
  
  
  /* Hash tracks/subtracks, limit visibility and calculate total image height: */
  
  // For multiple windows, sets height and visibility
  //       as well as making the flatTrack list.
  
  // TODO there is a chance that for pack and squish
  // I might need to trigger a track-height check here
  // since it is after all items are loaded for all windows
  // but before things are checked for overflow or limitedVis?
  // The fixed non-overflow tracks like knownGene used to initialize
  // ss and track height during loadItems(). That was delayed
  // because we now need all windows to be fully loaded before
  // calculating their joint ss layout and height.
  
  // set heights and visibilities.
  struct window *window = NULL;
  for(window=windows;window;window=window->next)
      {
      setGlobalsFromWindow(window);
      trackList = window->trackList;
      for (track = trackList; track != NULL; track = track->next)
  	{
  	if (tdbIsCompositeChild(track->tdb)) // When single track is requested via AJAX,
  	    {
  	    limitedVisFromComposite(track);  // it could be a subtrack
  	    }
  	else
  	    {
  	    limitVisibility(track);
  	    }
  
  	if (tdbIsComposite(track->tdb))
  	    {
  	    struct track *subtrack;
  	    for (subtrack = track->subtracks; subtrack != NULL; subtrack = subtrack->next)
  		{
  		if (!isSubtrackVisible(subtrack))
  		    continue;
  
  		// subtrack vis can be explicit or inherited from composite/view.
  		// Then it could be limited because of pixel height
  		limitedVisFromComposite(subtrack);
  
  		if (subtrack->limitedVis != tvHide)
  		    {
  		    subtrack->hasUi = track->hasUi;
  		    }
  		}
  	    }
  	}
      }
  trackList = windows->trackList;
  setGlobalsFromWindow(windows); // first window
  
  struct slName *orderedWiggles = NULL;
  char *sortTrack;
  char *wigOrder = NULL;
  if ((sortTrack = cgiOptionalString( "sortSim")) != NULL)
      {
      char buffer[1024];
      safef(buffer, sizeof buffer,  "simOrder_%s", sortTrack);
      wigOrder = cartString(cart, buffer);
      }
  
  if ((sortTrack = cgiOptionalString( "sortExp")) != NULL)
      {
      char buffer[1024];
      safef(buffer, sizeof buffer,  "expOrder_%s", sortTrack);
      wigOrder = cartString(cart, buffer);
      }
  
  if (wigOrder != NULL)
      {
      orderedWiggles = slNameListFromString(wigOrder, ' ');
      struct slName *name = orderedWiggles;
      // if we're sorting, remove existing sort order for this composite
      for(; name; name = name->next)
          {
          char buffer[1024];
          safef(buffer, sizeof buffer,  "%s_imgOrd", name->name);
          cartRemove(cart, buffer);
          }
      }
  
  // Construct flatTracks
  for (track = trackList; track != NULL; track = track->next)
      {
      if (tdbIsComposite(track->tdb))
          {
          struct track *subtrack;
          if (isCompositeInAggregate(track))
              flatTracksAdd(&flatTracks,track,cart, orderedWiggles);
          else
              {
-             boolean doCollapse = doCollapseEmptySubtracksNoMultiBed(track);
 -            boolean doCollapse = doCollapseEmptySubtracks(track);
++            boolean doHideEmpties = doHideEmptySubtracksNoMultiBed(track);
 +                // If multibed was found, it has been used to suppress loading,
 +                // and subtracks lacking items in window are already set hidden
              for (subtrack = track->subtracks; subtrack != NULL; subtrack = subtrack->next)
                  {
                  if (!isSubtrackVisible(subtrack))
                      continue;
  
                  if (!isLimitedVisHiddenForAllWindows(subtrack) && 
--                        !(doCollapse && slCount(subtrack->items) == 0))
++                        !(doHideEmpties && slCount(subtrack->items) == 0))
 +                        // Ignore subtracks with no items in window
                      {
                      flatTracksAdd(&flatTracks,subtrack,cart, orderedWiggles);
                      }
                  }
              }
          }
      else
  	{	
  	if (!isLimitedVisHiddenForAllWindows(track))
  	    {
  	    flatTracksAdd(&flatTracks,track,cart, orderedWiggles);
  	    }
  	}
      }
  flatTracksSort(&flatTracks); // Now we should have a perfectly good flat track list!
  
  if (orderedWiggles)
      {
      // save order to cart
      struct flatTracks *ft;
      char buffer[4096];
      int count = 1;
      for(ft = flatTracks; ft; ft = ft->next)
          {
          safef(buffer, sizeof buffer, "%s_imgOrd", ft->track->track);
          cartSetInt(cart, buffer, count++);
          }
      }
  
  // for each track, figure out maximum height needed from all windows
  for (flatTrack = flatTracks; flatTrack != NULL; flatTrack = flatTrack->next)
      {
      track = flatTrack->track;
  
      if (track->limitedVis == tvHide)
  	{
  	continue;
  	}
  
      setFlatTrackMaxHeight(flatTrack, fontHeight);
  
      }
  
  
  // fill out track->prevTrack, and check for maxSafeHeight
  boolean safeHeight = TRUE;
  /* firefox on Linux worked almost up to 34,000 at the default 620 width */
  #define maxSafeHeight   32000
  struct track *prevTrack = NULL;
  for (flatTrack = flatTracks,prevTrack=NULL; flatTrack != NULL; flatTrack = flatTrack->next)
      {
      track = flatTrack->track;
      assert(track->limitedVis != tvHide);
      // ORIG int totalHeight = pixHeight+trackPlusLabelHeight(track,fontHeight);
      int totalHeight = pixHeight+flatTrack->maxHeight;
      if (totalHeight > maxSafeHeight)
          {
          char numBuf[SMALLBUF];
          sprintLongWithCommas(numBuf, maxSafeHeight);
          if (safeHeight)  // Only one message
              warn("Image is over %s pixels high (%d pix) at the following track which is now "
                   "hidden:<BR>\"%s\".%s", numBuf, totalHeight, track->tdb->longLabel,
                   (flatTrack->next != NULL ?
                        "\nAdditional tracks may have also been hidden at this zoom level." : ""));
          safeHeight = FALSE;
  	struct track *winTrack;
  	for(winTrack=track;winTrack;winTrack=winTrack->nextWindow)
  	    {
  	    track->limitedVis = tvHide;
  	    track->limitedVisSet = TRUE;
  	    }
          }
      if (!isLimitedVisHiddenForAllWindows(track))
          {
  	struct track *winTrack;
  	for(winTrack=track;winTrack;winTrack=winTrack->nextWindow)
  	    { // TODO this is currently still only using one prev track value.
  	    winTrack->prevTrack = prevTrack; // Important for keeping track of conditional center labels!
  	    }
          // ORIG pixHeight += trackPlusLabelHeight(track, fontHeight);
  	if (!theImgBox) // prevTrack may have altered the height, so recalc height
  	    setFlatTrackMaxHeight(flatTrack, fontHeight);
  	pixHeight += flatTrack->maxHeight;
          prevTrack = track;
          }
      }
  
  // allocate hvg png of pixWidth, pixHeight
  imagePixelHeight = pixHeight;
  if (psOutput)
      {
      hvg = hvGfxOpenPostScript(pixWidth, pixHeight, psOutput);
      hvgSide = hvg; // Always only one image
      }
  else
      {
      boolean transparentImage = FALSE;
      if (theImgBox!=NULL)
          transparentImage = TRUE;   // transparent because BG (blue ruler lines) is separate image
  
      if (measureTiming)
          measureTime("Time at start of obtaining trash hgt png image file");
      trashDirFile(&pngTn, "hgt", "hgt", ".png");
      hvg = hvGfxOpenPng(pixWidth, pixHeight, pngTn.forCgi, transparentImage);
  
      if (theImgBox)
          {
          // Adds one single image for all tracks (COULD: build the track by track images)
          theOneImg = imgBoxImageAdd(theImgBox,pngTn.forHtml,NULL,pixWidth, pixHeight,FALSE);
          theSideImg = theOneImg; // Unlkess this is overwritten below, there is a single image
          }
  
      hvgSide = hvg; // Unless this is overwritten below, there is a single image
  
      if (theImgBox && theImgBox->showPortal && withLeftLabels)
          {
          // TODO: It would be great to make the two images smaller,
          //       but keeping both the same full size for now
          struct tempName pngTnSide;
          trashDirFile(&pngTnSide, "hgtSide", "side", ".png");
          hvgSide = hvGfxOpenPng(pixWidth, pixHeight, pngTnSide.forCgi, transparentImage);
  
          // Also add the side image
          theSideImg = imgBoxImageAdd(theImgBox,pngTnSide.forHtml,NULL,pixWidth, pixHeight,FALSE);
          hvgSide->rc = revCmplDisp;
          initColors(hvgSide);
          }
      }
  hvg->rc = revCmplDisp;
  initColors(hvg);
  
  /* Start up client side map. */
  hPrintf("<MAP id='map' Name=%s>\n", mapName);
  
  if (theImgBox == NULL)  // imageV2 highlighting is done by javascript. This does pdf and view-image highlight
      highlightRegions(cart, hvg, imagePixelHeight);
  
  for (window=windows; window; window=window->next)
      {
      /* Find colors to draw in. */
      findTrackColors(hvg, window->trackList);
      }
  
  
  // Good to go ahead and add all imgTracks regardless of buttons, left label, centerLabel, etc.
  if (theImgBox)
      {
      if (rulerMode != tvHide)
          {
          curImgTrack = imgBoxTrackFindOrAdd(theImgBox,NULL,RULER_TRACK_NAME,rulerMode,FALSE,
                                             IMG_FIXEDPOS); // No tdb, no centerLbl, not reorderable
          }
  
      for (flatTrack = flatTracks; flatTrack != NULL; flatTrack = flatTrack->next)
          {
          track = flatTrack->track;
  	if (!isLimitedVisHiddenForAllWindows(track))
              {
  	    struct track *winTrack;
  	    for (winTrack=track; winTrack; winTrack=winTrack->nextWindow)
  		{
  		if (winTrack->labelColor == winTrack->ixColor && winTrack->ixColor == 0)
  		    {
  		    winTrack->ixColor = hvGfxFindRgb(hvg, &winTrack->color);
  		    }
  		}
              int order = flatTrack->order;
              curImgTrack = imgBoxTrackFindOrAdd(theImgBox,track->tdb,NULL,track->limitedVis,
                                                 isCenterLabelIncluded(track),order);
-             if (trackShouldUseAjaxRetrieval(track))
-                 imgTrackMarkForAjaxRetrieval(curImgTrack,TRUE);
              }
          }
      }
  
  
  /* Draw mini-buttons. */
  
  if (withLeftLabels && psOutput == NULL)
      {
      int butOff;
      boolean grayButtonGroup = FALSE;
      struct group *lastGroup = NULL;
      y = gfxBorder;
      if (rulerMode != tvHide)
          {
          /* draw button for Base Position pseudo-track */
          int height = basePositionHeight;
          if (rulerCds)
              height += rulerTranslationHeight;
          if (theImgBox)
              {
              // Mini-buttons (side label slice) for ruler
              sliceHeight      = height + 1;
              sliceOffsetY     = 0;
              curImgTrack = imgBoxTrackFind(theImgBox,NULL,RULER_TRACK_NAME);
              curSlice    = imgTrackSliceUpdateOrAdd(curImgTrack,stButton,NULL,NULL,
                                                     sliceWidth[stButton],sliceHeight,
                                                     sliceOffsetX[stButton],sliceOffsetY);
              }
          else if (!trackImgOnly) // Side buttons only need to be drawn when drawing page with js
              {                   // advanced features off  // TODO: Should remove wasted pixels too
              drawGrayButtonBox(hvgSide, trackTabX, y, trackTabWidth, height, TRUE);
              }
          mapBoxTrackUi(hvgSide, trackTabX, y, trackTabWidth, height,
                        RULER_TRACK_NAME, RULER_TRACK_LABEL, "ruler");
          y += height + 1;
          }
  
      for (flatTrack = flatTracks; flatTrack != NULL; flatTrack = flatTrack->next)
          {
          track = flatTrack->track;
          int h, yStart = y, yEnd;
          if (track->limitedVis != tvHide)
              {
              // ORIG y += trackPlusLabelHeight(track, fontHeight);
  	    y += flatTrack->maxHeight;
              yEnd = y;
              h = yEnd - yStart - 1;
  
              /* alternate button colors for track groups*/
              if (track->group != lastGroup)
                  grayButtonGroup = !grayButtonGroup;
              lastGroup = track->group;
              if (theImgBox)
                  {
                  // Mini-buttons (side label slice) for tracks
                  sliceHeight      = yEnd - yStart;
                  sliceOffsetY     = yStart - 1;
                  curImgTrack = imgBoxTrackFind(theImgBox,track->tdb,NULL);
                  curSlice    = imgTrackSliceUpdateOrAdd(curImgTrack,stButton,NULL,NULL,
                                                         sliceWidth[stButton],sliceHeight,
                                                         sliceOffsetX[stButton],sliceOffsetY);
                  }
              else if (!trackImgOnly) // Side buttons only need to be drawn when drawing page
                  {                   // with js advanced features off
                  if (grayButtonGroup)
                      drawGrayButtonBox(hvgSide, trackTabX, yStart, trackTabWidth, h, track->hasUi);
                  else
                      drawBlueButtonBox(hvgSide, trackTabX, yStart, trackTabWidth, h, track->hasUi);
                  }
  
              if (track->hasUi)
                  {
                  if (tdbIsCompositeChild(track->tdb))
                      {
                      struct trackDb *parent = tdbGetComposite(track->tdb);
                      mapBoxTrackUi(hvgSide, trackTabX, yStart, trackTabWidth, (yEnd - yStart - 1),
                                    parent->track, parent->shortLabel, track->track);
                      }
                  else
                      mapBoxTrackUi(hvgSide, trackTabX, yStart, trackTabWidth, h, track->track,
                                    track->shortLabel, track->track);
                  }
              }
          }
      butOff = trackTabX + trackTabWidth;
      leftLabelX += butOff;
      leftLabelWidth -= butOff;
      }
  
  
  /* Draw left labels. */
  
  if (withLeftLabels)
      {
      if (theImgBox == NULL)
          {
          Color lightRed = hvGfxFindColorIx(hvgSide, 255, 180, 180);
  
          hvGfxBox(hvgSide, leftLabelX + leftLabelWidth, 0,
                   gfxBorder, pixHeight, lightRed);
          }
      y = gfxBorder;
      if (rulerMode != tvHide)
          {
          if (theImgBox)
              {
              // side label slice for ruler
              sliceHeight      = basePositionHeight + (rulerCds ? rulerTranslationHeight : 0) + 1;
              sliceOffsetY     = 0;
              curImgTrack = imgBoxTrackFind(theImgBox,NULL,RULER_TRACK_NAME);
              curSlice    = imgTrackSliceUpdateOrAdd(curImgTrack,stSide,theSideImg,NULL,
                                                     sliceWidth[stSide],sliceHeight,
                                                     sliceOffsetX[stSide],sliceOffsetY);
              (void) sliceMapFindOrStart(curSlice,RULER_TRACK_NAME,NULL); // No common linkRoot
              }
          if (baseTitle)
              {
              hvGfxTextRight(hvgSide, leftLabelX, y, leftLabelWidth-1, titleHeight,
                             MG_BLACK, font, WIN_TITLE_LABEL);
              y += titleHeight;
              }
          if (baseShowPos||baseShowAsm)
              {
              hvGfxTextRight(hvgSide, leftLabelX, y, leftLabelWidth-1, showPosHeight,
                             MG_BLACK, font, WIN_POS_LABEL);
              y += showPosHeight;
              }
          if (baseShowScaleBar)
              {
              y += scaleBarPad;
              hvGfxTextRight(hvgSide, leftLabelX, y, leftLabelWidth-1, scaleBarHeight,
                             MG_BLACK, font, SCALE_BAR_LABEL);
              y += scaleBarHeight + scaleBarPad;
              }
          if (baseShowRuler)
              {
              char rulerLabel[SMALLBUF];
              char *shortChromName = cloneString(chromName);
              safef(rulerLabel,ArraySize(rulerLabel),":%s",shortChromName);
              int labelWidth = mgFontStringWidth(font,rulerLabel);
              while ((labelWidth > 0) && (labelWidth > leftLabelWidth))
                  {
                  int len = strlen(shortChromName);
                  shortChromName[len-1] = 0;
                  safef(rulerLabel,ArraySize(rulerLabel),":%s",shortChromName);
                  labelWidth = mgFontStringWidth(font,rulerLabel);
                  }
              if (hvgSide->rc)
                  safef(rulerLabel,ArraySize(rulerLabel),":%s",shortChromName);
              else
                  safef(rulerLabel,ArraySize(rulerLabel),"%s:",shortChromName);
              hvGfxTextRight(hvgSide, leftLabelX, y, leftLabelWidth-1, rulerHeight,
                             MG_BLACK, font, rulerLabel);
              y += rulerHeight;
              freeMem(shortChromName);
              }
          if (zoomedToBaseLevel || rulerCds)
              {
              /* disable complement toggle for HIV because HIV is single stranded RNA */
              if (!hIsGsidServer())
                  drawComplementArrow(hvgSide,leftLabelX, y, leftLabelWidth-1, baseHeight, font);
              if (zoomedToBaseLevel)
                  y += baseHeight;
              }
          if (rulerCds)
              y += rulerTranslationHeight;
          }
      for (flatTrack = flatTracks; flatTrack != NULL; flatTrack = flatTrack->next)
          {
          track = flatTrack->track;
          if (track->limitedVis == tvHide)
              continue;
          if (theImgBox)
              {
              // side label slice for tracks
              //ORIG sliceHeight      = trackPlusLabelHeight(track, fontHeight);
  	    sliceHeight      = flatTrack->maxHeight;
              sliceOffsetY     = y;
              curImgTrack = imgBoxTrackFind(theImgBox,track->tdb,NULL);
              curSlice    = imgTrackSliceUpdateOrAdd(curImgTrack,stSide,theSideImg,NULL,
                                                     sliceWidth[stSide],sliceHeight,
                                                     sliceOffsetX[stSide],sliceOffsetY);
              (void) sliceMapFindOrStart(curSlice,track->tdb->track,NULL); // No common linkRoot
              }
-         if (trackShouldUseAjaxRetrieval(track))
-             y += REMOTE_TRACK_HEIGHT;
-         else
-             {
+ 
          boolean doWiggle = cartOrTdbBoolean(cart, track->tdb, "doWiggle" , FALSE);
          if (doWiggle)
              track->drawLeftLabels = wigLeftLabels;
      #ifdef IMAGEv2_NO_LEFTLABEL_ON_FULL
          if (theImgBox && track->limitedVis != tvDense)
              y += sliceHeight;
          else
      #endif ///def IMAGEv2_NO_LEFTLABEL_ON_FULL
              {
              setGlobalsFromWindow(windows); // use GLOBALS from first window
              int ynew = 0;
              /* rmskJoined tracks are non-standard in FULL mode
                 they are just their track height, not per-item height
               */
              if (startsWith("rmskJoined", track->track))
                  ynew = flatTrack->maxHeight + y;
              else
                  ynew = doLeftLabels(track, hvgSide, font, y);
  
              y += flatTrack->maxHeight;
              if ((ynew - y) > flatTrack->maxHeight)
                  { // TODO should be errAbort?
                  warn("doLeftLabels(y=%d) returned new y value %d that is too high - should be %d at most.",
                      y, ynew, flatTrack->maxHeight);
                  }
              }
          }
      }
-     }
  else
      {
      leftLabelX = leftLabelWidth = 0;
      }
  
  
  /* Draw guidelines. */
  
  if (virtMode && emAltHighlight)
      withGuidelines = TRUE;  // we cannot draw the alternating backgrounds without guidelines layer
  
  if (withGuidelines)
      {
      struct hvGfx *bgImg = hvg; // Default to the one image
      boolean exists = FALSE;
      if (theImgBox)
          {
          struct tempName gifBg;
          char base[64];
  	if (virtMode) // window separators
  	    {
  	    safecpy(base,sizeof(base),"winSeparators");  // non-reusable temp file
  	    trashDirFile(&gifBg, "hgt", base, ".png");
  	    exists = FALSE;
  	    }
  	else  // re-usable guidelines
  	    {
  	    safef(base,sizeof(base),"blueLines%d-%s%d-%d",pixWidth,(revCmplDisp?"r":""),fullInsideX,
  		  guidelineSpacing);  // reusable file needs width, leftLabel start and guidelines
  	    exists = trashDirReusableFile(&gifBg, "hgt", base, ".png");
  	    if (exists && cgiVarExists("hgt.reset")) // exists means don't remake bg image. However,
  		exists = FALSE;                      // for now, rebuild on "default tracks" request
  	    }
          if (!exists)
              {
              bgImg = hvGfxOpenPng(pixWidth, pixHeight, gifBg.forCgi, TRUE);
              bgImg->rc = revCmplDisp;
              }
          imgBoxImageAdd(theImgBox,gifBg.forHtml,NULL,pixWidth, pixHeight,TRUE); // Adds BG image
          }
  
      if (!exists)
          {
  
          hvGfxSetClip(bgImg, fullInsideX, 0, fullInsideWidth, pixHeight);
          y = gfxBorder;
  
  	if (virtMode)
  	    {
  	    // vertical windows separators
  
  	    if (emAltHighlight)
  		{
  		// light blue alternating backgrounds
  		Color lightBlue = hvGfxFindRgb(bgImg, &guidelineColor);
  		for (window=windows; window; window=window->next) // background under every other window
  		    {
  		    if (window->regionOdd)
  			hvGfxBox(bgImg, window->insideX, 0, window->insideWidth, pixHeight, lightBlue);
  		    }
  		}
  	    else
  		{
  		// red vertical lines
  		Color lightRed = hvGfxFindRgb(bgImg, &vertWindowSeparatorColor);
  		for (window=windows->next; window; window=window->next) // skip first window, not needed
  		    hvGfxBox(bgImg, window->insideX, 0, 1, pixHeight, lightRed);
  		}
  	    }
  	else
  	    {
  	    int x;
  	    Color lightBlue = hvGfxFindRgb(bgImg, &guidelineColor);
  	    for (x = fullInsideX+guidelineSpacing-1; x<pixWidth; x += guidelineSpacing)
  		hvGfxBox(bgImg, x, 0, 1, pixHeight, lightBlue);
  	    }
  
          hvGfxUnclip(bgImg);
          if (bgImg != hvg)
              hvGfxClose(&bgImg);
          }
      }
  
  
  /* Draw ruler */
  
  if (rulerMode != tvHide)
      {
      newWinWidth = calcNewWinWidth(cart,virtWinStart,virtWinEnd,fullInsideWidth);
  
      if (theImgBox)
  	{
  	// data slice for ruler
  	sliceHeight      = basePositionHeight + (rulerCds ? rulerTranslationHeight : 0) + 1;
  	sliceOffsetY     = 0;
  	curImgTrack = imgBoxTrackFind(theImgBox,NULL,RULER_TRACK_NAME);
  	curSlice    = imgTrackSliceUpdateOrAdd(curImgTrack,stData,theOneImg,rulerTtl,
  					       sliceWidth[stData],sliceHeight,
  					       sliceOffsetX[stData],sliceOffsetY);
  	(void) sliceMapFindOrStart(curSlice,RULER_TRACK_NAME,NULL); // No common linkRoot
  	}
  
      // need to have real winBaseCount to draw ruler scale
  
      for (window=windows; window; window=window->next)
  	{
  	setGlobalsFromWindow(window);
  
  	if (theImgBox)
  	    {
  	    // Show window positions as mouseover
  	    if (virtMode)
  		{
  		char position[256];
  		safef(position, sizeof position, "%s:%d-%d", window->chromName, window->winStart+1, window->winEnd);
  		int x = window->insideX;
  		if (revCmplDisp)
  		    x = tl.picWidth - (x + window->insideWidth);
  		imgTrackAddMapItem(curImgTrack, "#", position,
  		    x, sliceOffsetY, x+window->insideWidth, sliceOffsetY+sliceHeight, RULER_TRACK_NAME);
  		}
  
  	    }
  
  	y = doDrawRuler(hvg,&rulerClickHeight,rulerHeight,yAfterRuler,yAfterBases,font,
  			fontHeight,rulerCds, scaleBarTotalHeight, window);
  	}
  
      setGlobalsFromWindow(windows); // first window
  
      }
  
  
  
  /* Draw center labels. */
  
  if (withCenterLabels)
      {
      setGlobalsFromWindow(windows); // first window
  
      hvGfxSetClip(hvg, fullInsideX, gfxBorder, fullInsideWidth, pixHeight - 2*gfxBorder);
      y = yAfterRuler;
      for (flatTrack = flatTracks; flatTrack != NULL; flatTrack = flatTrack->next)
          {
          track = flatTrack->track;
          if (track->limitedVis == tvHide)
              continue;
  
          if (theImgBox)
              {
              // center label slice of tracks Must always make, even if the centerLabel is empty
              sliceHeight      = fontHeight;
              sliceOffsetY     = y;
              curImgTrack = imgBoxTrackFind(theImgBox,track->tdb,NULL);
              curSlice    = imgTrackSliceUpdateOrAdd(curImgTrack,stCenter,theOneImg,NULL,
                                                     sliceWidth[stData],sliceHeight,
                                                     sliceOffsetX[stData],sliceOffsetY);
              (void) sliceMapFindOrStart(curSlice,track->tdb->track,NULL); // No common linkRoot
              if (isCenterLabelConditional(track)) // sometimes calls track height, especially when no data there
  		{
                  imgTrackUpdateCenterLabelSeen(curImgTrack,isCenterLabelConditionallySeen(track) ?
                                                                              clNowSeen : clNotSeen);
  		}
              }
-         if (trackShouldUseAjaxRetrieval(track))
- 	    {
-             y += REMOTE_TRACK_HEIGHT;
- 	    }
-         else
- 	    {
+ 
          int savey = y; // GALT
          y = doCenterLabels(track, track, hvg, font, y, fullInsideWidth); // calls track height
          y = savey + flatTrack->maxHeight;
          }
-         }
      hvGfxUnclip(hvg);
  
      setGlobalsFromWindow(windows); // first window
      }
  
  
  
  /* Draw tracks. */
  
      { // brace allows local vars
  
      long lastTime = 0;
      y = yAfterRuler;
      if (measureTiming)
          lastTime = clock1000();
+ 
+     // first do predraw
      for (flatTrack = flatTracks; flatTrack != NULL; flatTrack = flatTrack->next)
          {
          track = flatTrack->track;
  
- 	// parallelize more this?:
- 	
-         //ORIG if (track->limitedVis == tvHide)
  	if (isLimitedVisHiddenForAllWindows(track))
              continue;
  
-         int centerLabelHeight = (isCenterLabelIncluded(track) ? fontHeight : 0);
-         int yStart = y + centerLabelHeight;
-         // ORIG int yEnd   = y + trackPlusLabelHeight(track, fontHeight);
- 	int yEnd   = y + flatTrack->maxHeight;
-         if (theImgBox)
-             {
-             // data slice of tracks
-             sliceOffsetY     = yStart;
-             sliceHeight      = yEnd - yStart - 1;
-             curImgTrack = imgBoxTrackFind(theImgBox,track->tdb,NULL);
-             if (sliceHeight > 0)
-                 {
-                 curSlice    = imgTrackSliceUpdateOrAdd(curImgTrack,stData,theOneImg,NULL,
-                                                        sliceWidth[stData],sliceHeight,
-                                                        sliceOffsetX[stData],sliceOffsetY);
-                 (void) sliceMapFindOrStart(curSlice,track->tdb->track,NULL); // No common linkRoot
-                 }
-             }
-         if (trackShouldUseAjaxRetrieval(track))
-             y += REMOTE_TRACK_HEIGHT;
-         else
- 	    {
- 	    int savey = y;
          struct track *winTrack;
  
          // do preDraw
          if (track->preDrawItems)
              {
              for (window=windows, winTrack=track; window; window=window->next, winTrack=winTrack->nextWindow)
                  {
                  setGlobalsFromWindow(window);
                  if (winTrack->limitedVis == tvHide)
                      {
                      warn("Draw tracks skipping %s because winTrack->limitedVis=hide", winTrack->track);
                      continue;
                      }
                  if (insideWidth >= 1)  // do not try to draw if width < 1.
                      {
                      doPreDrawItems(winTrack, hvg, font, y, &lastTime);
                      }
                  }
              }
  
          setGlobalsFromWindow(windows); // first window
          // do preDrawMultiRegion across all windows, e.g. wig autoScale
          if (track->preDrawMultiRegion)
              {
              track->preDrawMultiRegion(track);
              }
+         }
  
+     // now do the actual draw
+     for (flatTrack = flatTracks; flatTrack != NULL; flatTrack = flatTrack->next)
+         {
+         int savey = y;
+         struct track *winTrack;
+         track = flatTrack->track;
+ 	if (isLimitedVisHiddenForAllWindows(track))
+             continue;
+ 
+         int centerLabelHeight = (isCenterLabelIncluded(track) ? fontHeight : 0);
+         int yStart = y + centerLabelHeight;
+ 	int yEnd   = y + flatTrack->maxHeight;
+ 
+         if (theImgBox)
+             {
+             // data slice of tracks
+             sliceOffsetY     = yStart;
+             sliceHeight      = yEnd - yStart - 1;
+             curImgTrack = imgBoxTrackFind(theImgBox,track->tdb,NULL);
+             if (sliceHeight > 0)
+                 {
+                 curSlice    = imgTrackSliceUpdateOrAdd(curImgTrack,stData,theOneImg,NULL,
+                                                        sliceWidth[stData],sliceHeight,
+                                                        sliceOffsetX[stData],sliceOffsetY);
+                 (void) sliceMapFindOrStart(curSlice,track->tdb->track,NULL); // No common linkRoot
+                 }
+             }
          // doDrawItems
          for (window=windows, winTrack=track; window; window=window->next, winTrack=winTrack->nextWindow)
              {
              setGlobalsFromWindow(window);
              if (winTrack->limitedVis == tvHide)
                  {
                  warn("Draw tracks skipping %s because winTrack->limitedVis=hide", winTrack->track);
                  continue;
                  }
              if (insideWidth >= 1)  // do not try to draw if width < 1.
                  {
                  int ynew = doDrawItems(winTrack, hvg, font, y, &lastTime);
                  if ((ynew-y) > flatTrack->maxHeight)  // so compiler does not complain ynew is not used.
                      errAbort("oops track too high!");
                  }
              }
          setGlobalsFromWindow(windows); // first window
          y = savey + flatTrack->maxHeight;
- 	    }
  
          if (theImgBox && tdbIsCompositeChild(track->tdb) &&
                  (track->limitedVis == tvDense ||
                   (track->limitedVis == tvPack && centerLabelHeight == 0)))
              mapBoxToggleVis(hvg, 0, yStart,tl.picWidth, sliceHeight,track);
              // Strange mapBoxToggleLogic handles reverse complement itself so x=0,width=tl.picWidth
  
          if (yEnd != y)
              warn("Slice height for track %s does not add up.  Expecting %d != %d actual",
                   track->shortLabel, yEnd - yStart - 1, y - yStart);
          }
  
      calcWiggleOrdering(cart, flatTracks);
      y++;
      }
  
  /* post draw tracks leftLabels */
  
  /* if a track can draw its left labels, now is the time since it
   *  knows what exactly happened during drawItems
   */
  if (withLeftLabels)
      {
      y = yAfterRuler;
      for (flatTrack = flatTracks; flatTrack != NULL; flatTrack = flatTrack->next)
          {
          track = flatTrack->track;
          if (track->limitedVis == tvHide)
              continue;
          if (theImgBox)
              {
              // side label slice of tracks
  	    sliceHeight      = flatTrack->maxHeight;
              sliceOffsetY     = y;
              curImgTrack = imgBoxTrackFind(theImgBox,track->tdb,NULL);
              curSlice    = imgTrackSliceUpdateOrAdd(curImgTrack,stSide,theSideImg,NULL,
                                                     sliceWidth[stSide],sliceHeight,
                                                     sliceOffsetX[stSide],sliceOffsetY);
              (void) sliceMapFindOrStart(curSlice,track->tdb->track,NULL); // No common linkRoot
              }
  
-         if (trackShouldUseAjaxRetrieval(track))
-             y += REMOTE_TRACK_HEIGHT;
-         else if (track->drawLeftLabels != NULL)
+         if (track->drawLeftLabels != NULL)
  	    {
  	    setGlobalsFromWindow(windows);
              y = doOwnLeftLabels(track, hvgSide, font, y);
  	    setGlobalsFromWindow(windows); // first window
  	    }
          else
  	    y += flatTrack->maxHeight;
          }
      }
  
  /* Make map background. */
  
  y = yAfterRuler;
  for (flatTrack = flatTracks; flatTrack != NULL; flatTrack = flatTrack->next)
      {
      track = flatTrack->track;
      if (track->limitedVis != tvHide)
          {
          if (theImgBox)
              {
              // Set imgTrack in case any map items will be set
  	    sliceHeight      = flatTrack->maxHeight;
              sliceOffsetY     = y;
              curImgTrack = imgBoxTrackFind(theImgBox,track->tdb,NULL);
              }
  
  	setGlobalsFromWindow(windows); // first window
          doTrackMap(track, hvg, y, fontHeight, trackPastTabX, trackPastTabWidth);
  
  	y += flatTrack->maxHeight;
          }
      }
  
  /* Finish map. */
  hPrintf("</MAP>\n");
  
  // turn off inPlaceUpdate when rows in imgTbl can arbitrarily reappear and disappear (see redmine #7306 and #6944)
  jsonObjectAdd(jsonForClient, "inPlaceUpdate", newJsonBoolean(withLeftLabels && withCenterLabels));
  jsonObjectAdd(jsonForClient, "rulerClickHeight", newJsonNumber(rulerClickHeight));
  if(newWinWidth)
      {
      jsonObjectAdd(jsonForClient, "newWinWidth", newJsonNumber(newWinWidth));
      }
  
  /* Save out picture and tell html file about it. */
  if (hvgSide != hvg)
      hvGfxClose(&hvgSide);
  hvGfxClose(&hvg);
  if (measureTiming)
      measureTime("Time completed writing trash hgt png image file");
  
  #ifdef SUPPORT_CONTENT_TYPE
  char *type = cartUsualString(cart, "hgt.contentType", "html");
  if(sameString(type, "jsonp"))
      {
      struct jsonElement *json = newJsonObject(newHash(8));
  
      printf("Content-Type: application/json\n\n");
      errAbortSetDoContentType(FALSE);
      jsonObjectAdd(json, "track", newJsonString(cartString(cart, "hgt.trackNameFilter")));
      jsonObjectAdd(json, "height", newJsonNumber(pixHeight));
      jsonObjectAdd(json, "width", newJsonNumber(pixWidth));
      jsonObjectAdd(json, "img", newJsonString(pngTn.forHtml));
      printf("%s(", cartString(cart, "jsonp"));
      hPrintEnable();
      jsonPrint((struct jsonElement *) json, NULL, 0);
      hPrintDisable();
      printf(")\n");
      return;
      }
  else if(sameString(type, "png") || sameString(type, "pdf") || sameString(type, "eps"))
      {
      // following code bypasses html and return png's directly - see redmine 4888
      char *file;
      if(sameString(type, "pdf"))
          {
          printf("Content-Disposition: filename=hgTracks.pdf\nContent-Type: application/pdf\n\n");
          file = convertEpsToPdf(psOutput);
          unlink(psOutput);
          }
      else if(sameString(type, "eps"))
          {
          printf("Content-Disposition: filename=hgTracks.eps\nContent-Type: application/eps\n\n");
          file = psOutput;
          }
      else
          {
          printf("Content-Disposition: filename=hgTracks.png\nContent-Type: image/png\n\n");
          file = pngTn.forCgi;
          }
  
      char buf[4096];
      FILE *fd = fopen(file, "r");
      if(fd == NULL)
          // fail some other way (e.g. HTTP 500)?
          errAbort("Couldn't open png for reading");
      while (TRUE)
          {
          size_t n = fread(buf, 1, sizeof(buf), fd);
          if(n)
              fwrite(buf, 1, n, stdout);
          else
              break;
          }
      fclose(fd);
      unlink(file);
      return;
      }
  #endif///def SUPPORT_CONTENT_TYPE
  
  if (theImgBox)
      {
      imageBoxDraw(theImgBox);
      // If a portal was established, then set the global dimensions back to the portal size
      if (imgBoxPortalDimensions(theImgBox,NULL,NULL,NULL,NULL,&virtWinStart,&virtWinEnd,&(tl.picWidth),NULL))
          {
          pixWidth = tl.picWidth;
          virtWinBaseCount = virtWinEnd - virtWinStart;
          fullInsideWidth = tl.picWidth - gfxBorder - fullInsideX;
          }
      imgBoxFree(&theImgBox);
      }
  else
      {
      char *titleAttr = "title='click or drag mouse in base position track to zoom in'";
      hPrintf("<IMG SRC='%s' BORDER=1 WIDTH=%d HEIGHT=%d USEMAP=#%s %s id='trackMap'",
              pngTn.forHtml, pixWidth, pixHeight, mapName, titleAttr);
      hPrintf("><BR>\n");
      }
  flatTracksFree(&flatTracks);
  }
  
  void makeHgGenomeTrackVisible(struct track *track)
  /* This turns on a track clicked from hgGenome, even if it was previously */
  /* hidden manually and there are cart vars to support that. */
  {
  struct hashEl *hels;
  struct hashEl *hel;
  char prefix[SMALLBUF];
  /* First check if the click was from hgGenome.  If not, leave. */
  /* get the names of the tracks in the cart */
  safef(prefix, sizeof(prefix), "%s_", hggGraphPrefix);
  hels = cartFindPrefix(cart, prefix);
  /* loop through them and compare them to the track passed into this */
  /* function. */
  for (hel = hels; hel != NULL; hel = hel->next)
      {
      struct trackDb *subtrack;
      char *subtrackName = hel->val;
      /* check non-subtrack. */
      if (sameString(track->tdb->track, subtrackName))
  	{
  	track->visibility = tvFull;
  	track->tdb->visibility = tvFull;
  	cartSetString(cart, track->tdb->track, "full");
  	}
      else if (track->tdb->subtracks != NULL)
  	{
  	struct slRef *tdbRef, *tdbRefList = trackDbListGetRefsToDescendants(track->tdb->subtracks);
  	for (tdbRef = tdbRefList; tdbRef != NULL; tdbRef = tdbRef->next)
  	    {
  	    subtrack = tdbRef->val;
  	    if (sameString(subtrack->track, subtrackName))
  		{
  		char selName[SMALLBUF];
  		track->visibility = tvFull;
  		cartSetString(cart, track->tdb->track, "full");
  		track->tdb->visibility = tvFull;
  		subtrack->visibility = tvFull;
  		safef(selName, sizeof(selName), "%s_sel", subtrackName);
  		cartSetBoolean(cart, selName, TRUE);
  		}
  	    }
  	slFreeList(&tdbRefList);
  	}
      }
  hashElFreeList(&hels);
  }
  
  struct sqlConnection *remoteTrackConnection(struct track *tg)
  /* Get a connection to remote database as specified in remoteSql settings... */
  {
  if (!tg->isRemoteSql)
      {
      internalErr();
      return NULL;
      }
  else
      {
      return sqlConnectRemote(tg->remoteSqlHost, tg->remoteSqlUser, tg->remoteSqlPassword,
                              tg->remoteSqlDatabase);
      }
  }
  
  void addTdbListToTrackList(struct trackDb *tdbList, char *trackNameFilter,
  	struct track **pTrackList)
  /* Convert a list of trackDb's to tracks, and append these to trackList. */
  {
  struct trackDb *tdb, *next;
  struct track *track;
  TrackHandler handler;
  tdbSortPrioritiesFromCart(cart, &tdbList);
  for (tdb = tdbList; tdb != NULL; tdb = next)
      {
      next = tdb->next;
      if(trackNameFilter != NULL && strcmp(trackNameFilter, tdb->track))
          // suppress loading & display of all tracks except for the one passed in via trackNameFilter
          continue;
      if (sameString(tdb->type, "downloadsOnly")) // These tracks should not even be seen by hgTracks.
          continue;
      track = trackFromTrackDb(tdb);
      track->hasUi = TRUE;
      if (slCount(tdb->subtracks) != 0)
          {
          tdbSortPrioritiesFromCart(cart, &(tdb->subtracks));
  	if (trackDbLocalSetting(tdb, "compositeTrack"))
  	    makeCompositeTrack(track, tdb);
  	else if (trackDbLocalSetting(tdb, "container"))
  	    makeContainerTrack(track, tdb);
          }
      else
          {
          handler = lookupTrackHandlerClosestToHome(tdb);
          if (handler != NULL)
              handler(track);
          }
      if (cgiVarExists("hgGenomeClick"))
  	makeHgGenomeTrackVisible(track);
      if (track->loadItems == NULL)
          warn("No load handler for %s; possible missing trackDb `type' or `subTrack' attribute", tdb->track);
      else if (track->drawItems == NULL)
          warn("No draw handler for %s", tdb->track);
      else
          slAddHead(pTrackList, track);
      }
  }
  
  void loadFromTrackDb(struct track **pTrackList)
  /* Load tracks from database, consulting handler list. */
  {
  char *trackNameFilter = cartOptionalString(cart, "hgt.trackNameFilter");
  struct trackDb *tdbList;
  if(trackNameFilter == NULL)
      tdbList = hTrackDb(database);
  else
      {
      tdbList = hTrackDbForTrack(database, trackNameFilter);
  
      if (tdbList && tdbList->parent)        // we want to give the composite parent a chance to load and set options
          {
          while(tdbList->parent)
              {
              if (tdbList->parent->subtracks == NULL)     // we don't want to go up to a supertrack
                  break;
              tdbList = tdbList->parent;
              }
          trackNameFilter = tdbList->track;
          }
      }
  addTdbListToTrackList(tdbList, trackNameFilter, pTrackList);
  }
  
  static int getScoreFilter(char *trackName)
  /* check for score filter configuration setting */
  {
  char optionScoreStr[256];
  
  safef(optionScoreStr, sizeof(optionScoreStr), "%s.scoreFilter", trackName);
  return cartUsualInt(cart, optionScoreStr, 0);
  }
  
  void ctLoadSimpleBed(struct track *tg)
  /* Load the items in one custom track - just move beds in
   * window... */
  {
  struct customTrack *ct = tg->customPt;
  struct bed *bed, *nextBed, *list = NULL;
  int scoreFilter = getScoreFilter(ct->tdb->track);
  
  if (ct->dbTrack)
      {
      int fieldCount = ct->fieldCount;
      int rowOffset;
      char **row;
      struct sqlConnection *conn =
          hAllocConn(CUSTOM_TRASH);
      struct sqlResult *sr = NULL;
  
      sr = hRangeQuery(conn, ct->dbTableName, chromName, winStart, winEnd,
                       NULL, &rowOffset);
      while ((row = sqlNextRow(sr)) != NULL)
          {
          bed = bedLoadN(row+rowOffset, fieldCount);
  	    if (scoreFilter && bed->score < scoreFilter)
  		continue;
  	slAddHead(&list, bed);
  	}
      hFreeConn(&conn);
      }
  else
      {
      for (bed = ct->bedList; bed != NULL; bed = nextBed)
  	{
  	nextBed = bed->next;
  	if (bed->chromStart < winEnd && bed->chromEnd > winStart
  		&& sameString(chromName, bed->chrom))
  	    {
  	    if (scoreFilter && bed->score < scoreFilter)
  		continue;
  	    slAddHead(&list, bed);
  	    }
  	}
      }
  slSort(&list, bedCmp);
  tg->items = list;
  }
  
  void ctLoadBed9(struct track *tg)
  /* Convert bed info in window to linked feature. */
  {
  struct customTrack *ct = tg->customPt;
  struct bed *bed;
  struct linkedFeatures *lfList = NULL, *lf;
  boolean useItemRgb = FALSE;
  int scoreFilter = getScoreFilter(ct->tdb->track);
  
  useItemRgb = bedItemRgb(ct->tdb);
  
  if (ct->dbTrack)
      {
      int rowOffset;
      char **row;
      struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH);
      struct sqlResult *sr = NULL;
  
      sr = hRangeQuery(conn, ct->dbTableName, chromName, winStart, winEnd,
                       NULL, &rowOffset);
      while ((row = sqlNextRow(sr)) != NULL)
          {
          bed = bedLoadN(row+rowOffset, 9);
  	if (scoreFilter && bed->score < scoreFilter)
  	    continue;
  	bed8To12(bed);
  	lf = lfFromBed(bed);
  	if (useItemRgb)
  	    {
  	    lf->extra = (void *)USE_ITEM_RGB;   /* signal for coloring */
  	    lf->filterColor=bed->itemRgb;
  	    }
  	slAddHead(&lfList, lf);
  	}
      hFreeConn(&conn);
      }
  else
      {
      for (bed = ct->bedList; bed != NULL; bed = bed->next)
  	{
          if (scoreFilter && bed->score < scoreFilter)
              continue;
  	if (bed->chromStart < winEnd && bed->chromEnd > winStart
  		&& sameString(chromName, bed->chrom))
  	    {
  	    bed8To12(bed);
  	    lf = lfFromBed(bed);
  	    if (useItemRgb)
  		{
  		lf->extra = (void *)USE_ITEM_RGB;   /* signal for coloring */
  		lf->filterColor=bed->itemRgb;
  		}
  	    slAddHead(&lfList, lf);
  	    }
  	}
      }
  slReverse(&lfList);
  slSort(&lfList, linkedFeaturesCmp);
  tg->items = lfList;
  }
  
  
  void ctLoadBed8(struct track *tg)
  /* Convert bed info in window to linked feature. */
  {
  struct customTrack *ct = tg->customPt;
  struct bed *bed;
  struct linkedFeatures *lfList = NULL, *lf;
  int scoreFilter = getScoreFilter(ct->tdb->track);
  
  if (ct->dbTrack)
      {
      int fieldCount = ct->fieldCount;
      int rowOffset;
      char **row;
      struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH);
      struct sqlResult *sr = NULL;
  
      sr = hRangeQuery(conn, ct->dbTableName, chromName, winStart, winEnd,
                       NULL, &rowOffset);
      while ((row = sqlNextRow(sr)) != NULL)
          {
          bed = bedLoadN(row+rowOffset, fieldCount);
  	    if (scoreFilter && bed->score < scoreFilter)
  		continue;
  	bed8To12(bed);
  	lf = lfFromBed(bed);
  	slAddHead(&lfList, lf);
  	}
      hFreeConn(&conn);
      }
  else
      {
      for (bed = ct->bedList; bed != NULL; bed = bed->next)
  	{
  	if (scoreFilter && bed->score < scoreFilter)
  	    continue;
  	if (bed->chromStart < winEnd && bed->chromEnd > winStart
  		&& sameString(chromName, bed->chrom))
  	    {
  	    bed8To12(bed);
  	    lf = lfFromBed(bed);
  	    slAddHead(&lfList, lf);
  	    }
  	}
      }
  slReverse(&lfList);
  slSort(&lfList, linkedFeaturesCmp);
  tg->items = lfList;
  }
  
  void ctLoadGappedBed(struct track *tg)
  /* Convert bed info in window to linked feature. */
  {
  struct customTrack *ct = tg->customPt;
  struct bed *bed;
  struct linkedFeatures *lfList = NULL, *lf;
  boolean useItemRgb = FALSE;
  int scoreFilter = getScoreFilter(ct->tdb->track);
  
  useItemRgb = bedItemRgb(ct->tdb);
  
  if (ct->dbTrack)
      {
      int fieldCount = ct->fieldCount;
      int rowOffset;
      char **row;
      struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH);
      struct sqlResult *sr = NULL;
  
      sr = hRangeQuery(conn, ct->dbTableName, chromName, winStart, winEnd,
                       NULL, &rowOffset);
      while ((row = sqlNextRow(sr)) != NULL)
          {
          bed = bedLoadN(row+rowOffset, fieldCount);
  	lf = lfFromBed(bed);
  	    if (scoreFilter && bed->score < scoreFilter)
  		continue;
  	if (useItemRgb)
  	    {
  	    lf->extra = (void *)USE_ITEM_RGB;   /* signal for coloring */
  	    lf->filterColor=bed->itemRgb;
  	    }
  	slAddHead(&lfList, lf);
  	}
      hFreeConn(&conn);
      }
  else
      {
      for (bed = ct->bedList; bed != NULL; bed = bed->next)
  	{
          if (scoreFilter && bed->score < scoreFilter)
              continue;
  	if (bed->chromStart < winEnd && bed->chromEnd > winStart
  		&& sameString(chromName, bed->chrom))
  	    {
  	    lf = lfFromBed(bed);
  	    if (useItemRgb)
  		{
  		lf->extra = (void *)USE_ITEM_RGB; /* signal for coloring */
  		lf->filterColor=bed->itemRgb;
  		}
  	    slAddHead(&lfList, lf);
  	    }
  	}
      }
  slReverse(&lfList);
  slSort(&lfList, linkedFeaturesCmp);
  tg->items = lfList;
  }
  
  void ctLoadColoredExon(struct track *tg)
  /* Convert bed info in window to linked features series for custom track. */
  {
  struct customTrack *ct = tg->customPt;
  struct bed *bed;
  struct linkedFeaturesSeries *lfsList = NULL, *lfs;
  if (ct->dbTrack)
      {
      int fieldCount = ct->fieldCount;
      int rowOffset;
      char **row;
      struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH);
      struct sqlResult *sr = NULL;
      sr = hRangeQuery(conn, ct->dbTableName, chromName, winStart, winEnd,
                       NULL, &rowOffset);
      while ((row = sqlNextRow(sr)) != NULL)
          {
          bed = bedLoadN(row+rowOffset, fieldCount);
  	lfs = lfsFromColoredExonBed(bed);
  	slAddHead(&lfsList, lfs);
  	}
      hFreeConn(&conn);
      }
  else
      {
      for (bed = ct->bedList; bed != NULL; bed = bed->next)
  	{
  	if (bed->chromStart < winEnd && bed->chromEnd > winStart
  		&& sameString(chromName, bed->chrom))
  	    {
  	    lfs = lfsFromColoredExonBed(bed);
  	    slAddHead(&lfsList, lfs);
  	    }
  	}
      }
  slReverse(&lfsList);
  slSort(&lfsList, linkedFeaturesSeriesCmp);
  tg->items = lfsList;
  }
  
  char *ctMapItemName(struct track *tg, void *item)
  /* Return composite item name for custom tracks. */
  {
  char *itemName = tg->itemName(tg, item);
  static char buf[512];
  if (strlen(itemName) > 0)
      safef(buf, sizeof(buf), "%s %s", ctFileName, itemName);
  else
      safef(buf, sizeof(buf), "%s NoItemName", ctFileName);
  return buf;
  }
  
  
  void coloredExonMethodsFromCt(struct track *tg)
  /* same as coloredExonMethods but different loader. */
  {
  linkedFeaturesSeriesMethods(tg);
  tg->loadItems = ctLoadColoredExon;
  tg->canPack = TRUE;
  }
  
  void dontLoadItems(struct track *tg)
  /* No-op loadItems when we aren't going to try. */
  {
  }
  
  struct track *newCustomTrack(struct customTrack *ct)
  /* Make up a new custom track. */
  {
  struct track *tg = NULL;
  struct trackDb *tdb = ct->tdb;
  boolean useItemRgb = FALSE;
  char *typeOrig = tdb->type;
  char *typeDupe = cloneString(typeOrig);
  char *typeParam = typeDupe;
  char *type = nextWord(&typeParam);
  
  if (ct->dbTrack)
      {
      // make sure we can connect
      struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH);
      hFreeConn(&conn);
      }
  
  useItemRgb = bedItemRgb(tdb);
  
  if (sameString(type, "maf"))
      {
      tg = trackFromTrackDb(tdb);
      tg->canPack = TRUE;
  
      wigMafMethods(tg, tdb, 0, NULL);
      if (!ct->dbTrack)
          errAbort("custom maf tracks must be in database");
  
  
      struct mafPriv *mp;
      AllocVar(mp);
      mp->ct = ct;
  
      tg->customPt = mp;
      tg->nextItemButtonable = FALSE;
      }
  else if (sameString(type, "wig"))
      {
      tg = trackFromTrackDb(tdb);
      if (ct->dbTrack)
          tg->loadItems = wigLoadItems;
      else
          tg->loadItems = ctWigLoadItems;
      tg->customPt = ct;
      tg->nextItemButtonable = FALSE;
      }
  else if (sameString(type, "bigWig"))
      {
      tg = trackFromTrackDb(tdb);
      tg->bbiFile = ct->bbiFile;
      tg->nextItemButtonable = FALSE;
-     if (trackShouldUseAjaxRetrieval(tg))
-         tg->loadItems = dontLoadItems;
      }
  else if (sameString(type, "bigBed")|| sameString(type, "bigGenePred") ||
          sameString(type, "bigNarrowPeak") || sameString(type, "bigPsl") ||
          sameString(type, "bigMaf")|| sameString(type, "bigChain") ||
          sameString(type, "bigLolly") || 
          sameString(type, "bigBarChart") || sameString(type, "bigInteract"))
      {
      struct bbiFile *bbi = ct->bbiFile;
  
      /* Find field counts, and from that revise the tdb->type to be more complete. */
      char extra = (bbi->fieldCount > bbi->definedFieldCount ? '+' : '.');
      char typeBuf[64];
      if (sameString(type, "bigGenePred"))
  	safef(typeBuf, sizeof(typeBuf), "bigGenePred");
      else if (sameString(type, "bigNarrowPeak"))
  	safef(typeBuf, sizeof(typeBuf), "bigNarrowPeak");
      else if (sameString(type, "bigChain"))
  	safef(typeBuf, sizeof(typeBuf), "bigChain");
      else if (sameString(type, "bigMaf"))
  	safef(typeBuf, sizeof(typeBuf), "bigMaf");
      else if (sameString(type, "bigPsl"))
  	safef(typeBuf, sizeof(typeBuf), "bigPsl");
      else if (sameString(type, "bigBarChart"))
  	safef(typeBuf, sizeof(typeBuf), "bigBarChart");
      else if (sameString(type, "bigLolly"))
  	safef(typeBuf, sizeof(typeBuf), "bigLolly");
      else if (sameString(type, "bigInteract"))
  	safef(typeBuf, sizeof(typeBuf), "bigInteract");
      else
  	safef(typeBuf, sizeof(typeBuf), "bigBed %d %c", bbi->definedFieldCount, extra);
      tdb->type = cloneString(typeBuf);
  
      /* Finish wrapping track around tdb. */
      tg = trackFromTrackDb(tdb);
      tg->bbiFile = bbi;
      tg->nextItemButtonable = TRUE;
-     if (trackShouldUseAjaxRetrieval(tg))
-         tg->loadItems = dontLoadItems;
      }
  else if (sameString(type, "bedGraph"))
      {
      tg = trackFromTrackDb(tdb);
      tg->canPack = FALSE;
      tg->customPt = ct;
      ct->wigFile = ctFileName;
      tg->mapItemName = ctMapItemName;
      tg->nextItemButtonable = FALSE;
      }
  else if (sameString(type, "bed"))
      {
      tg = trackFromTrackDb(tdb);
      if (ct->fieldCount < 8)
  	{
  	tg->loadItems = ctLoadSimpleBed;
  	}
      else if (useItemRgb && ct->fieldCount == 9)
  	{
  	tg->loadItems = ctLoadBed9;
  	}
      else if (ct->fieldCount < 12)
  	{
  	tg->loadItems = ctLoadBed8;
  	}
      else if (ct->fieldCount == 15)
  	{
  	char *theType = trackDbSetting(tdb, "type");
  	if (theType && sameString(theType, "expRatio"))
  	    {
  	    tg = trackFromTrackDb(tdb);
  	    expRatioMethodsFromCt(tg);
  	    }
  	else
  	    tg->loadItems = ctLoadGappedBed;
  	}
      else
  	{
  	tg->loadItems = ctLoadGappedBed;
  	}
      tg->mapItemName = ctMapItemName;
      tg->canPack = TRUE;
      tg->nextItemButtonable = TRUE;
      tg->customPt = ct;
      }
  else if (sameString(type, "chromGraph"))
      {
      tdb->type = NULL;   /* Swap out type for the moment. */
      tg = trackFromTrackDb(tdb);
      chromGraphMethodsCt(tg);
      tg->nextItemButtonable = FALSE;
      tdb->type = typeOrig;
      }
  else if (sameString(type, "array"))
      {
      tg = trackFromTrackDb(tdb);
      expRatioMethodsFromCt(tg);
      tg->nextItemButtonable = TRUE;
      tg->customPt = ct;
      }
  else if (sameString(type, "coloredExon"))
      {
      tg = trackFromTrackDb(tdb);
      coloredExonMethodsFromCt(tg);
      tg->nextItemButtonable = TRUE;
      tg->customPt = ct;
      }
  else if (sameString(type, "encodePeak"))
      {
      tg = trackFromTrackDb(tdb);
      encodePeakMethodsCt(tg);
      tg->nextItemButtonable = TRUE;
      tg->customPt = ct;
      }
  else if (sameString(type, "bam"))
      {
      tg = trackFromTrackDb(tdb);
      tg->customPt = ct;
      bamMethods(tg);
-     if (trackShouldUseAjaxRetrieval(tg))
-         tg->loadItems = dontLoadItems;
      tg->mapItemName = ctMapItemName;
      }
  else if (sameString(type, "vcfTabix"))
      {
      tg = trackFromTrackDb(tdb);
      tg->customPt = ct;
      vcfTabixMethods(tg);
-     if (trackShouldUseAjaxRetrieval(tg))
-         tg->loadItems = dontLoadItems;
      tg->mapItemName = ctMapItemName;
      }
  else if (sameString(type, "vcf"))
      {
      tg = trackFromTrackDb(tdb);
      tg->customPt = ct;
      vcfMethods(tg);
      tg->mapItemName = ctMapItemName;
      }
  else if (sameString(type, "makeItems"))
      {
      tg = trackFromTrackDb(tdb);
      makeItemsMethods(tg);
      tg->nextItemButtonable = TRUE;
      tg->customPt = ct;
      }
  else if (sameString(type, "bedTabix")  || sameString(type, "longTabix"))
      {
      knetUdcInstall();
      tg = trackFromTrackDb(tdb);
      tg->customPt = ct;
      tg->mapItemName = ctMapItemName; /* must be here to see ctMapItemName */
      }
  else if (sameString(type, "bedDetail"))
      {
      tg = trackFromTrackDb(tdb);
      bedDetailCtMethods(tg, ct);
      tg->mapItemName = ctMapItemName; /* must be here to see ctMapItemName */
      }
      else if (sameString(type, "adjacency"))
      {
      extern void adjacencyMethods(struct track *track);
  
      tg = trackFromTrackDb(tdb);
      adjacencyMethods(tg);
      //tg->mapItemName = ctMapItemName;
      tg->customPt = ct;
      }
  else if (sameString(type, "pgSnp"))
      {
      tg = trackFromTrackDb(tdb);
      pgSnpCtMethods(tg);
      //tg->mapItemName = ctMapItemName;
      tg->customPt = ct;
      }
  else if (sameString(type, "barChart"))
      {
      tg = trackFromTrackDb(tdb);
      barChartCtMethods(tg);
      tg->customPt = ct;
      }
  else if (sameString(type, "interact"))
      {
      tg = trackFromTrackDb(tdb);
      interactCtMethods(tg);
      tg->customPt = ct;
      }
  else if (sameString(type, "hic"))
      {
      tg = trackFromTrackDb(tdb);
      hicCtMethods(tg);
      tg->customPt = ct;
      }
  else
      {
      errAbort("Unrecognized custom track type %s", type);
      }
  tg->hasUi = TRUE;
  tg->customTrack = TRUE;// Explicitly declare this a custom track for flatTrack ordering
  
  freez(&typeDupe);
  return tg;
  }
  
  char *getPositionFromCustomTracks()
  /* Parses custom track data to get the position variable
   * return - The first chromosome position variable found in the
   * custom track data.  */
  {
  char *pos = NULL;
  struct slName *bl = NULL;
  
  ctList = customTracksParseCart(database, cart, &browserLines, &ctFileName);
  
  for (bl = browserLines; bl != NULL; bl = bl->next)
      {
      char *words[96];
      int wordCount;
      char *dupe = cloneString(bl->name);
  
      wordCount = chopLine(dupe, words);
      if (wordCount >= 3)
          {
          char *command = words[1];
          if (sameString(command, "position"))
              pos = cloneString(words[2]);
          }
      freez(&dupe);
      if (pos != NULL)
          break;
      }
  return pos;
  }
  
  void loadCustomTracks(struct track **pTrackList)
  /* Load up custom tracks and append to list. */
  {
  struct customTrack *ct;
  struct track *tg;
  struct slName *bl;
  
  /* build up browser lines from cart variables set by hgCustom */
  char *visAll = cartCgiUsualString(cart, "hgt.visAllFromCt", NULL);
  if (visAll)
      {
      char buf[SMALLBUF];
      safef(buf, sizeof buf, "browser %s %s", visAll, "all");
      slAddTail(&browserLines, slNameNew(buf));
      }
  struct hashEl *visEl;
  struct hashEl *visList = cartFindPrefix(cart, "hgtct.");
  for (visEl = visList; visEl != NULL; visEl = visEl->next)
      {
      char buf[256];
      safef(buf, sizeof buf, "browser %s %s", cartString(cart, visEl->name),
                  chopPrefix(cloneString(visEl->name)));
      slAddTail(&browserLines, slNameNew(buf));
      cartRemove(cart, visEl->name);
      }
  hashElFreeList(&visList);
  
  /* The loading is now handled by getPositionFromCustomTracks(). */
  /* Process browser commands in custom track. */
  for (bl = browserLines; bl != NULL; bl = bl->next)
      {
      char *words[96];
      int wordCount;
  
      wordCount = chopLine(bl->name, words);
      if (wordCount > 1)
          {
  	char *command = words[1];
  	if (sameString(command, "hide")
              || sameString(command, "dense")
              || sameString(command, "pack")
              || sameString(command, "squish")
              || sameString(command, "full"))
  	    {
  	    if (wordCount > 2)
  		{
  		int i;
  		for (i=2; i<wordCount; ++i)
  		    {
  		    char *s = words[i];
  		    struct track *tg;
  		    boolean toAll = sameWord(s, "all");
  		    for (tg = *pTrackList; tg != NULL; tg = tg->next)
  			{
  			if (toAll || sameString(s, tg->track))
  			    {
  			    if (hTvFromString(command) == tg->tdb->visibility)
  				/* remove if setting to default vis */
  				cartRemove(cart, tg->track);
  			    else
  				cartSetString(cart, tg->track, command);
  			    /* hide or show supertrack enclosing this track */
  			    if (tdbIsSuperTrackChild(tg->tdb))
  				{
  				assert(tg->tdb->parentName != NULL);
  				cartSetString(cart, tg->tdb->parentName,
  					    (sameString(command, "hide") ?
  						"hide" : "show"));
  				}
  			    }
  			}
  		    }
  		}
  	    }
  	else if (sameString(command, "position"))
  	    {
  	    if (wordCount < 3)
  		errAbort("Expecting 3 words in browser position line");
  	    if (!hgIsChromRange(database, words[2]))
  		errAbort("browser position needs to be in chrN:123-456 format");
  	    hgParseChromRange(database, words[2], &chromName, &winStart, &winEnd);
  
  		/*Fix a start window of -1 that is returned when a custom track position
  		  begins at 0
  		*/
  		if (winStart < 0)
  		    {
  		    winStart = 0;
  		    }
  	    }
  	else if (sameString(command, "pix"))
  	    {
  	    if (wordCount != 3)
  		errAbort("Expecting 3 words in pix line");
  	    trackLayoutSetPicWidth(&tl, words[2]);
  	    }
  	}
      }
  for (ct = ctList; ct != NULL; ct = ct->next)
      {
      hasCustomTracks = TRUE;
      tg = newCustomTrack(ct);
      slAddHead(pTrackList, tg);
      }
  }
  
  void loadTrackHubs(struct track **pTrackList, struct grp **pGrpList)
  /* Load up stuff from data hubs and append to lists. */
  {
  struct trackDb *tdbList = hubCollectTracks(database, pGrpList);
  
  addTdbListToTrackList(tdbList, NULL, pTrackList);
  }
  
  boolean restrictionEnzymesOk()
  /* Check to see if it's OK to do restriction enzymes. */
  {
  return (sqlDatabaseExists("hgFixed") && hTableExists("hgFixed", "cutters") &&
          hTableExists("hgFixed", "rebaseRefs") &&
          hTableExists("hgFixed", "rebaseCompanies"));
  }
  
  static void setSuperTrackHasVisibleMembers(struct trackDb *tdb)
  /* Determine if any member tracks are visible -- currently
   * recording this in the parent's visibility setting */
  {
  tdb->visibility = tvDense;
  }
  
  boolean superTrackHasVisibleMembers(struct trackDb *tdb)
  /* Determine if any member tracks are visible -- currently
   * recording this in the parent's visibility setting */
  {
  if (!tdbIsSuper(tdb))
      return FALSE;
  return (tdb->visibility != tvHide);
  }
  
  int hubCmpAlpha(const void *va, const void *vb)
  /* Compare to sort hubs based on name */
  {
  const struct trackHub *a = *((struct trackHub **)va);
  const struct trackHub *b = *((struct trackHub **)vb);
  
  return strcmp(a->shortLabel, b->shortLabel);
  }
  
  static void rPropagateGroup(struct track *track, struct group *group)
  // group should spread to multiple levels of children.
  {
  struct track *subtrack = track->subtracks;
  for ( ;subtrack != NULL;subtrack = subtrack->next)
      {
      subtrack->group = group;
      rPropagateGroup(subtrack, group);
      }
  }
  
  static void groupTracks(struct track **pTrackList,
  	struct group **pGroupList, struct grp *grpList, int vis)
  /* Make up groups and assign tracks to groups.
   * If vis is -1, restore default groups to tracks. */
  {
  struct group *unknown = NULL;
  struct group *group, *list = NULL;
  struct hash *hash = newHash(8);
  struct track *track;
  struct trackRef *tr;
  struct grp* grps = hLoadGrps(database);
  struct grp *grp;
  float minPriority = 100000; // something really large
  boolean foundMap = FALSE;
  
  /* build group objects from database. */
  for (grp = grps; grp != NULL; grp = grp->next)
      {
      if (sameString(grp->name, "map"))
          foundMap = TRUE;
      /* deal with group reordering */
      float priority = grp->priority;
      // we want to get the minimum priority over 1 (which is custom tracks)
      if ((priority > 1.0) && (priority < minPriority)) minPriority = priority;
      if (withPriorityOverride)
          {
          char cartVar[512];
          safef(cartVar, sizeof(cartVar), "%s.priority",grp->name);
          if (vis != -1)
              priority = (float)cartUsualDouble(cart, cartVar, grp->priority);
          if (priority == grp->priority)
              cartRemove(cart, cartVar);
          }
      /* create group object; add to list and hash */
      AllocVar(group);
      group->name = cloneString(grp->name);
      group->label = cloneString(grp->label);
      group->defaultPriority = grp->priority;
      group->priority = priority;
      group->defaultIsClosed = grp->defaultIsClosed;
      slAddHead(&list, group);
      hashAdd(hash, grp->name, group);
      }
  grpFreeList(&grps);
  
  double priorityInc;
  double priority = 1.00001;
  if (grpList)
      {
      minPriority -= 1.0;             // priority is 1-based
      // the idea here is to get enough room between priority 1
      // (which is custom tracks) and the group with the next
      // priority number, so that the hub nestle inbetween the
      // custom tracks and everything else at the top of the list
      // of track groups
      priorityInc = (0.9 * minPriority) / slCount(grpList);
      priority = 1.0 + priorityInc;
      }
  for(; grpList; grpList = grpList->next)
      {
      AllocVar(group);
      group->name = cloneString(grpList->name);
      group->label = cloneString(grpList->label);
      group->defaultPriority = group->priority = priority;
      priority += priorityInc;
      slAddHead(&list, group);
      hashAdd(hash, group->name, group);
      }
  //
  // If there isn't a map group, make one and set the priority so it will be right after custom tracks
  // and hub groups.
  if (!foundMap)
      {
      AllocVar(group);
      group->name = cloneString("map");
      group->label = cloneString("Mapping and Sequencing");
      group->defaultPriority = priority;
      group->priority = priority;
      group->defaultIsClosed = FALSE;
      slAddHead(&list, group);
      hashAdd(hash, "map", group);
      }
  
  
  /* Loop through tracks and fill in their groups.
   * If necessary make up an unknown group. */
  for (track = *pTrackList; track != NULL; track = track->next)
      {
      /* handle track reordering feature -- change group assigned to track */
      if (withPriorityOverride)
          {
          char *groupName = NULL;
          char cartVar[256];
  
          /* belt and suspenders -- accomodate inconsistent track/trackDb
           * creation.  Note -- with code cleanup, these default variables
           * could be retired, and the tdb versions used as defaults */
          if (!track->defaultGroupName)
              {
              if (track->tdb && track->tdb->grp)
                  track->defaultGroupName = cloneString(track->tdb->grp);
              else
                  track->defaultGroupName = cloneString("other");
              }
          if (tdbIsSuperTrackChild(track->tdb))
              {
              assert(track->tdb->parentName != NULL);
              /* supertrack member must be in same group as its super */
              /* determine supertrack group */
              safef(cartVar, sizeof(cartVar), "%s.group",track->tdb->parentName);
              groupName = cloneString(                                              //1
                      cartUsualString(cart, cartVar, track->tdb->parent->grp));
              track->tdb->parent->grp = cloneString(groupName);                     //2
              }
          else
              {
              /* get group */
              safef(cartVar, sizeof(cartVar), "%s.group",track->track);
              groupName = cloneString(                                              //1
                      cartUsualString(cart, cartVar, track->defaultGroupName));
              }
          if (vis == -1)
              groupName = track->defaultGroupName;                                  //0
          track->groupName = cloneString(groupName);  // wasting a few clones!      //3
          if (sameString(groupName, track->defaultGroupName))
              cartRemove(cart, cartVar);
  
          /* get priority */
          safef(cartVar, sizeof(cartVar), "%s.priority",track->track);
          float priority = (float)cartUsualDouble(cart, cartVar,
                                                      track->defaultPriority);
          /* remove cart variables that are the same as the trackDb settings */
  /*  UGLY - add me back when tdb->priority is no longer pre-clobbered by cart var value
          if (priority == track->defaultPriority)
              cartRemove(cart, cartVar);
  */
          track->priority = priority;
          }
  
      /* assign group object to track */
      if (track->groupName == NULL)
          group = NULL;
      else
  	group = hashFindVal(hash, track->groupName);
      if (group == NULL)
          {
  	if (unknown == NULL)
  	    {
  	    AllocVar(unknown);
  	    unknown->name = cloneString("other");
  	    unknown->label = cloneString("other");
  	    unknown->priority = 1000000;
  	    slAddHead(&list, unknown);
  	    }
  	group = unknown;
  	}
      track->group = group;
      rPropagateGroup(track, group);
      }
  
  /* Sort tracks by combined group/track priority, and
   * then add references to track to group. */
  slSort(pTrackList, tgCmpPriority);
  for (track = *pTrackList; track != NULL; track = track->next)
      {
      AllocVar(tr);
      tr->track = track;
      slAddHead(&track->group->trackList, tr);
      }
  
  /* Straighten things out, clean up, and go home. */
  for (group = list; group != NULL; group = group->next)
      slReverse(&group->trackList);
  slSort(&list, gCmpPriority);
  hashFree(&hash);
  *pGroupList = list;
  }
  
  void groupTrackListAddSuper(struct cart *cart, struct group *group)
  /* Construct a new track list that includes supertracks, sort by priority,
   * and determine if supertracks have visible members.
   * Replace the group track list with this new list.
   * Shared by hgTracks and configure page to expand track list,
   * in contexts where no track display functions (which don't understand
   * supertracks) are invoked. */
  {
  struct trackRef *newList = NULL, *tr, *ref;
  struct hash *superHash = hashNew(8);
  
  if (!group || !group->trackList)
      return;
  for (tr = group->trackList; tr != NULL; tr = tr->next)
      {
      struct track *track = tr->track;
      AllocVar(ref);
      ref->track = track;
      slAddHead(&newList, ref);
      if (tdbIsSuperTrackChild(track->tdb))
          {
          assert(track->tdb->parentName != NULL);
          if (hTvFromString(cartUsualString(cart, track->track,
                          hStringFromTv(track->tdb->visibility))) != tvHide)
              setSuperTrackHasVisibleMembers(track->tdb->parent);
          assert(track->parent == NULL);
          track->parent = hashFindVal(superHash, track->tdb->parentName);
          if (track->parent)
              continue;
          /* create track and reference for the supertrack */
          struct track *superTrack = track->parent = trackFromTrackDb(track->tdb->parent);
          track->parent = superTrack;
          if (trackHash != NULL)
              hashAddUnique(trackHash,superTrack->track,superTrack);
          superTrack->hasUi = TRUE;
          superTrack->group = group;
          superTrack->groupName = cloneString(group->name);
          superTrack->defaultGroupName = cloneString(group->name);
  
          /* handle track reordering */
          char cartVar[256];
          safef(cartVar, sizeof(cartVar), "%s.priority",track->tdb->parentName);
          float priority = (float)cartUsualDouble(cart, cartVar,
                                          track->tdb->parent->priority);
          /* remove cart variables that are the same as the trackDb settings */
          if (priority == track->tdb->parent->priority)
              cartRemove(cart, cartVar);
          superTrack->priority = priority;
  
          AllocVar(ref);
          ref->track = superTrack;
          slAddHead(&newList, ref);
          hashAdd(superHash, track->tdb->parentName, superTrack);
          }
      }
  slSort(&newList, trackRefCmpPriority);
  hashFree(&superHash);
  /* we could free the old track list here, but it's a trivial amount of mem */
  group->trackList = newList;
  }
  
  void topButton(char *var, char *label)
  /* create a 3 or 4-char wide button for top line of display.
   * 3 chars wide for odd-length labels, 4 for even length.
   * Pad with spaces so label is centered */
  {
  char paddedLabel[5] = "    ";
  int len = strlen(label);
  if (len > 4)
      {
      /* truncate */
      /* or maybe errabort ? */
      label[3] = 0;
      len = 4;
      }
  if (len % 2 != 0)
      paddedLabel[3] = 0;
  if (len == strlen(paddedLabel))
      strcpy(paddedLabel, label);
  else
      {
      int i;
      for (i=0; i<len; i++)
          paddedLabel[i+1] = label[i];
      }
  hButtonWithOnClick(var, paddedLabel, NULL, "return imageV2.navigateButtonClick(this);");
  }
  
  void limitSuperTrackVis(struct track *track)
  /* Limit track visibility by supertrack parent */
  {
  if (tdbIsSuperTrackChild(track->tdb))
      {
      assert(track->tdb->parent != NULL);
      if (sameString("hide", cartUsualString(cart, track->tdb->parent->track,
                                             track->tdb->parent->isShow ? "show" : "hide")))
          track->visibility = tvHide;
      }
  }
  
  struct track *rFindTrackWithTable(char *tableName, struct track *trackList)
  /* Recursively search through track list looking for first one that matches table. */
  {
  struct track *track;
  for (track = trackList; track != NULL; track = track->next)
      {
      if (sameString(tableName, track->table))
           return track;
      struct track *subTrack = rFindTrackWithTable(tableName, track->subtracks);
      if (subTrack != NULL)
           return subTrack;
      }
  return NULL;
  }
  
  static void setSearchedTrackToPackOrFull(struct track *trackList)
  // Open track associated with search position if any. Also open its parents if any.
  {
  if (NULL != hgp && NULL != hgp->tableList && NULL != hgp->tableList->name)
      {
      char *tableName = hgp->tableList->name;
      struct track *matchTrack = rFindTrackWithTable(tableName, trackList);
      if (matchTrack != NULL)
          tdbSetCartVisibility(matchTrack->tdb, cart, hCarefulTrackOpenVis(database, matchTrack->track));
      }
  }
  
  struct track *getTrackList( struct group **pGroupList, int vis)
  /* Return list of all tracks, organizing by groups.
   * If vis is -1, restore default groups to tracks.
   * Shared by hgTracks and configure page. */
  {
  struct track *track, *trackList = NULL;
  registerTrackHandlers();
  /* Load regular tracks, blatted tracks, and custom tracks.
   * Best to load custom last. */
  loadFromTrackDb(&trackList);
  if (measureTiming)
      measureTime("Time after trackDbLoad ");
  if (pcrResultParseCart(database, cart, NULL, NULL, NULL))
      slSafeAddHead(&trackList, pcrResultTg());
  if (userSeqString != NULL)
      slSafeAddHead(&trackList, userPslTg());
  slSafeAddHead(&trackList, oligoMatchTg());
  if (restrictionEnzymesOk())
      {
      slSafeAddHead(&trackList, cuttersTg());
      }
  if (wikiTrackEnabled(database, NULL))
      {
      addWikiTrack(&trackList);
      struct sqlConnection *conn = wikiConnect();
      if (sqlTableExists(conn, "variome"))
          addVariomeWikiTrack(&trackList);
      wikiDisconnect(&conn);
      }
  
  struct grp *grpList = NULL;
  if (cartOptionalString(cart, "hgt.trackNameFilter") == NULL)
      { // If a single track was asked for and it is from a hub, then it is already in trackList
      loadTrackHubs(&trackList, &grpList);
      }
  loadCustomTracks(&trackList);
  groupTracks( &trackList, pGroupList, grpList, vis);
  setSearchedTrackToPackOrFull(trackList);
  boolean hideTracks = cgiOptionalString( "hideTracks") != NULL;
  if (hideTracks)
      changeTrackVis(groupList, NULL, tvHide);    // set all top-level tracks to hide
  
  /* Get visibility values if any from ui. */
  struct hash *superTrackHash = newHash(5);  // cache whether supertrack is hiding tracks or not
  char buffer[4096];
  
  for (track = trackList; track != NULL; track = track->next)
      {
      // deal with any supertracks we're seeing for the first time
      if (tdbIsSuperTrackChild(track->tdb))
          {
          struct hashEl *hel = NULL;
  
          if ((hel = hashLookup(superTrackHash, track->tdb->parent->track)) == NULL)   // we haven't seen this guy
              {
              // first deal with visibility of super track
              char *s = hideTracks ? cgiOptionalString(track->tdb->parent->track) : cartOptionalString(cart, track->tdb->parent->track);
              if (s)
                  {
                  track->tdb->parent->visibility = hTvFromString(s) ;
                  cartSetString(cart, track->tdb->parent->track, s);
                  }
              else if (startsWith("hub_", track->tdb->parent->track))
                  {
                  s = hideTracks ? cgiOptionalString( trackHubSkipHubName(track->tdb->parent->track)) :  cartOptionalString( cart, trackHubSkipHubName(track->tdb->parent->track));
                  if (s)
                      {
                      cartSetString(cart, track->tdb->parent->track, s);
                      cartRemove(cart, trackHubSkipHubName(track->tdb->parent->track)); // remove the undecorated version
                      track->tdb->parent->visibility = hTvFromString(s) ;
                      }
                  }
              
              // now look to see if we have a _hideKids statement to turn off all subtracks (including the current one)
              unsigned hideKids = 0;
              char *usedThis = buffer;
              safef(buffer, sizeof buffer, "%s_hideKids", track->tdb->parent->track);
  
              s = cartOptionalString(cart, buffer);
              if (s == NULL && startsWith("hub_", track->tdb->parent->track))
                  s = cartOptionalString(cart, usedThis = trackHubSkipHubName(buffer));
  
              if (s != NULL)
                  {
                  hideKids = 1;
                  cartRemove(cart, usedThis);  // we don't want this hanging out in the cart
                  }
  
              // mark this as having been addressed
              hel = hashAddInt(superTrackHash, track->tdb->parent->track, hideKids );  
              }
  
          if ( ptToInt(hel->val) == 1)    // we want to hide this track
              {
              if (tvHide == track->tdb->visibility)
                  /* remove if setting to default vis */
                  cartRemove(cart, track->track);
              else
                  cartSetString(cart, track->track, "hide");
              track->visibility = tvHide;
              }
          }
      
      // we use cgiOptionString because the above code may have turned off the track in the cart if
      // the user requested that all the default tracks be turned off
      char *s = hideTracks ? cgiOptionalString(track->track) : cartOptionalString(cart, track->track);
  
      if (s != NULL)
          {
          if (!track->limitedVisSet)
              {
              track->visibility = hTvFromString(s); 
              cartSetString(cart, track->track, s);
              }
          }
      else
          {
          // maybe this track is on the URL without the hub_ prefix
          if (startsWith("hub_", track->track))
              s = cgiOptionalString(trackHubSkipHubName(track->track));
          if (s != NULL && !track->limitedVisSet)
              {
              track->visibility = hTvFromString(s);
              cartSetString(cart, track->track, s);   // add the decorated visibility to the cart
              cartRemove(cart, trackHubSkipHubName(track->track)); // remove the undecorated version
              }
          }
  
      // now deal with composite track children
      if (tdbIsComposite(track->tdb))
          {
          char *usedThis = buffer;
  
          // first check to see if we've been asked to hide all the subtracks
          boolean hideKids = FALSE;
          safef(buffer, sizeof buffer, "%s_hideKids", track->track);
  
          s = cartOptionalString(cart, buffer);
          if (s == NULL && startsWith("hub_", track->track))
              s = cartOptionalString(cart, usedThis = trackHubSkipHubName(buffer));
          if (s != NULL)
              hideKids = TRUE;
          cartRemove(cart, usedThis);   // we don't want these _hideKids variables in the cart
  
          // now see if we have any specified visibilities
          struct track *subtrack;
          for (subtrack = track->subtracks; subtrack != NULL; subtrack = subtrack->next)
              {
              boolean undecoratedVis = FALSE;
              char *s = hideTracks ? cgiOptionalString( subtrack->track) : cartOptionalString(cart, subtrack->track);
              if (s == NULL && startsWith("hub_", subtrack->track))
                  {
                  undecoratedVis = TRUE;
                  s = hideTracks ? cgiOptionalString(trackHubSkipHubName(subtrack->track)) : cartOptionalString(cart, trackHubSkipHubName(subtrack->track));
                  }
  
              safef(buffer, sizeof buffer, "%s_sel", subtrack->track);
              if (s != NULL)
                  {
                  subtrack->visibility = hTvFromString(s);
                  cartSetString(cart, subtrack->track, s);
                  if (sameString("hide", s))
                      cartSetString(cart, buffer, "0");
                  else
                      cartSetString(cart, buffer, "1");
                  if (undecoratedVis)
                      cartRemove(cart, trackHubSkipHubName(subtrack->track)); // remove the undecorated version
                  }
              else if (hideKids && isSubtrackVisible(subtrack))
                  cartSetString(cart, buffer, "0");
              }
          }
      }
  return trackList;
  }
  
  char *collapseGroupVar(char *name)
  /* Construct cart variable name for collapsing group */
  {
  static char varName[256];
  safef(varName, sizeof(varName),
          "%s%s_%s_%s", "hgt", "group", name, "close");
  return (cloneString(varName));
  }
  
  boolean isCollapsedGroup(struct group *grp)
  /* Determine if group is collapsed */
  {
  return cartUsualInt(cart, collapseGroupVar(grp->name), grp->defaultIsClosed);
  }
  
  void collapseGroupGoodies(boolean isOpen, boolean wantSmallImage,
                                  char **img, char **indicator, char **otherState)
  /* Get image, char representation of image, and 'otherState' (1 or 0)
   * for a group, based on whether it is collapsed, and whether we want
   * larger or smaller image for collapse box */
  {
  if (otherState)
      *otherState = (isOpen ? "1" : "0");
  if (indicator)
      *indicator = (isOpen ? "-" : "+");
  if (img)
      {
      if (wantSmallImage)
          *img = (isOpen ? "../images/remove_sm.gif" : "../images/add_sm.gif");
      else
          *img = (isOpen ? "../images/remove.gif" : "../images/add.gif");
      }
  }
  
  void collapseGroup(char *name, boolean doCollapse)
  /* Set cart variable to cause group to collapse */
  {
  cartSetBoolean(cart, collapseGroupVar(name), doCollapse);
  }
  
  void myControlGridStartCell(struct controlGrid *cg, boolean isOpen, char *id)
  /* Start a new cell in control grid; support Javascript open/collapsing by including id's in tr's.
     id is used as id prefix (a counter is added to make id's unique). */
  {
  static int counter = 1;
  if (cg->columnIx == cg->columns)
      controlGridEndRow(cg);
  if (!cg->rowOpen)
      {
  #if 0
      /* This is unnecessary, b/c we can just use a blank display attribute to show the element rather
         than figuring out what the browser specific string is to turn on display of the tr;
         however, we may want to put in browser specific strings in the future, so I'm leaving this
         code in as a reference. */
      char *ua = getenv("HTTP_USER_AGENT");
      char *display = ua && stringIn("MSIE", ua) ? "block" : "table-row";
  #endif
      // use counter to ensure unique tr id's (prefix is used to find tr's in javascript).
      printf("<tr %sid='%s-%d'>", isOpen ? "" : "style='display: none' ", id, counter++);
      cg->rowOpen = TRUE;
      }
  if (cg->align)
      printf("<td align=%s>", cg->align);
  else
      printf("<td>");
  }
  
  static void pruneRedundantCartVis(struct track *trackList)
  /* When the config page or track form has been submitted, there usually
   * are many track visibility cart variables that have not been changed
   * from the default.  To keep down cart bloat, prune those out before we
   * save the cart.  changeTrackVis does this too, but this is for the
   * more common case where track visibilities are tweaked. */
  {
  struct track *track;
  for (track = trackList; track != NULL; track = track->next)
      {
      char *cartVis = cartOptionalString(cart, track->track);
      if (cartVis != NULL && hTvFromString(cartVis) == track->tdb->visibility)
          cartRemove(cart, track->track);
      }
  }
  
  static int getMaxWindowToDraw(struct trackDb *tdb)
  /* If trackDb setting maxWindowToDraw exists and is a sensible size, return it, else 0. */
  {
  if (tdb == NULL)
      return 0;
  char *maxWinToDraw = trackDbSettingClosestToHome(tdb, "maxWindowToDraw");
  if (isNotEmpty(maxWinToDraw))
      {
      unsigned maxWTD = sqlUnsigned(maxWinToDraw);
      if (maxWTD > 1)
          return maxWTD;
      }
  return 0;
  }
  
  static void drawMaxWindowWarning(struct track *tg, int seqStart, int seqEnd, struct hvGfx *hvg,
                                   int xOff, int yOff, int width, MgFont *font, Color color,
                                   enum trackVisibility vis)
  /* This is a stub drawItems handler to be swapped in for the usual drawItems when the window
   * size is larger than the threshold specified by trackDb setting maxWindowToDraw. */
  {
  int maxWinToDraw = getMaxWindowToDraw(tg->tdb);
  char commafied[256];
  sprintLongWithCommas(commafied, maxWinToDraw);
  char message[512];
  safef(message, sizeof(message), "zoom in to <= %s bases to view items", commafied);
  Color yellow = hvGfxFindRgb(hvg, &undefinedYellowColor);
  hvGfxBox(hvg, xOff, yOff, width, tg->heightPer, yellow);
  hvGfxTextCentered(hvg, xOff, yOff, width, tg->heightPer, MG_BLACK, font, message);
  }
  
  static void checkMaxWindowToDraw(struct track *tg)
  /* If (winEnd - winStart) > trackDb setting maxWindowToDraw, force track to a dense line
   * that will ask the user to zoom in closer to see track items and return TRUE so caller
   * can skip loading items. */
  {
  int maxWinToDraw = getMaxWindowToDraw(tg->tdb);
  if (tdbIsComposite(tg->tdb))
      {
      struct track *subtrack;
      for (subtrack = tg->subtracks;  subtrack != NULL;  subtrack = subtrack->next)
  	{
  	if (!isSubtrackVisible(subtrack))
  	    continue;
  	maxWinToDraw = getMaxWindowToDraw(subtrack->tdb);
  	if (maxWinToDraw > 1 && (winEnd - winStart) > maxWinToDraw)
  	    {
  	    subtrack->loadItems = dontLoadItems;
  	    subtrack->drawItems = drawMaxWindowWarning;
  	    subtrack->limitedVis = tvDense;
  	    subtrack->limitedVisSet = TRUE;
  	    }
  	}
      }
  else if (maxWinToDraw > 1 && (winEnd - winStart) > maxWinToDraw)
      {
      tg->loadItems = dontLoadItems;
      tg->drawItems = drawMaxWindowWarning;
      tg->limitedVis = tvDense;
      tg->limitedVisSet = TRUE;
      }
  }
  
- static void checkCollapseEmptySubtracks(struct track *tg)
++static void checkHideEmptySubtracks(struct track *tg)
 +/* Suppress queries on subtracks w/o data in window (identified from multiIntersect file) */
 +{
 +if (!tdbIsComposite(tg->tdb))
 +    return;
 +struct hash *nonEmptySubtracksHash = getNonEmptySubtracks(tg);
 +if (!nonEmptySubtracksHash)
 +    return;
 +struct track *subtrack;
 +for (subtrack = tg->subtracks; subtrack != NULL; subtrack = subtrack->next)
 +    {
 +    if (!isSubtrackVisible(subtrack))
 +        continue;
 +    if (!hashLookup(nonEmptySubtracksHash, subtrack->track))
 +        {
 +        subtrack->loadItems = dontLoadItems;
 +        subtrack->limitedVis = tvHide;
 +        subtrack->limitedVisSet = TRUE;
 +        }
 +    }
 +}
 +
  void printTrackInitJavascript(struct track *trackList)
  {
  hPrintf("<input type='hidden' id='%s' name='%s' value=''>\n", hgtJsCommand, hgtJsCommand);
  }
  
  void jsCommandDispatch(char *command, struct track *trackList)
  /* Dispatch a command sent to us from some javaScript event.
   * This gets executed after the track list is built, but before
   * the track->loadItems methods are called.  */
  {
  if (startsWithWord("makeItems", command))
      makeItemsJsCommand(command, trackList, trackHash);
  else
      warn("Unrecognized jsCommand %s", command);
  }
  
  void parentChildCartCleanup(struct track *trackList,struct cart *newCart,struct hash *oldVars)
  /* When composite/view settings changes, remove subtrack specific vis
     When superTrackChild is found and selected, shape superTrack to match. */
  {
  struct lm *lm = lmInit(0);	/* Speed tweak cleanup with scatch memory pool. */
  struct track *track = trackList;
  for (;track != NULL; track = track->next)
      {
      boolean shapedByubtrackOverride = FALSE;
      boolean cleanedByContainerSettings = FALSE;
  
      // Top-down 'cleanup' MUST GO BEFORE bottom up reshaping.
      cleanedByContainerSettings = cartTdbTreeCleanupOverrides(track->tdb,newCart,oldVars, lm);
  
      if (tdbIsContainer(track->tdb))
          {
          shapedByubtrackOverride = cartTdbTreeReshapeIfNeeded(cart,track->tdb);
          if (shapedByubtrackOverride)
              track->visibility = tdbVisLimitedByAncestors(cart,track->tdb,TRUE,TRUE);
          }
      if ((shapedByubtrackOverride || cleanedByContainerSettings)
      &&  tdbIsSuperTrackChild(track->tdb))  // Either cleanup may require supertrack intervention
          {   // Need to update track visibility
              // Unfortunately, since supertracks are not in trackList, this occurs on superChildren,
              // So now we need to find the supertrack and take changed cart values of its children
          struct slRef *childRef;
          for (childRef = track->tdb->parent->children;childRef != NULL;childRef = childRef->next)
              {
              struct trackDb * childTdb = childRef->val;
              struct track *child = hashFindVal(trackHash, childTdb->track);
              if (child != NULL && child->track!=NULL)
                  {
                  char *cartVis = cartOptionalString(cart,child->track);
                  if (cartVis)
                      child->visibility = hTvFromString(cartVis);
                  }
              }
          }
      }
  lmCleanup(&lm);
  }
  
  
  struct paraFetchData
      {
      struct paraFetchData *next;
      struct track *track;
      boolean done;
      };
  
  static boolean isTrackForParallelLoad(struct track *track)
  /* Is this a track that should be loaded in parallel ? */
  {
  char *bdu = trackDbSetting(track->tdb, "bigDataUrl");
  return (startsWithWord("bigWig"  , track->tdb->type)
       || startsWithWord("mathWig"  , track->tdb->type)
       || startsWithWord("bigBed"  , track->tdb->type)
       || startsWithWord("bigPsl"  , track->tdb->type)
       || startsWithWord("bigNarrowPeak"  , track->tdb->type)
       || startsWithWord("bigGenePred"  , track->tdb->type)
       || startsWithWord("bigChain"  , track->tdb->type)
       || startsWithWord("bam"     , track->tdb->type)
       || startsWithWord("halSnake", track->tdb->type)
       || startsWithWord("bigLolly", track->tdb->type)
       || startsWithWord("vcfTabix", track->tdb->type))
       // XX code-review: shouldn't we error abort if the URL is not valid?
       && (bdu && isValidBigDataUrl(bdu, FALSE))
       && !(containsStringNoCase(bdu, "dl.dropboxusercontent.com"))
       && (track->subtracks == NULL);
  }
  
  static void findLeavesForParallelLoad(struct track *trackList, struct paraFetchData **ppfdList)
  /* Find leaves of track tree that are remote network resources for parallel-fetch loading */
  {
  struct track *track;
  if (!trackList)
      return;
  for (track = trackList; track != NULL; track = track->next)
      {
  
      if (track->visibility != tvHide)
  	{
  	if (isTrackForParallelLoad(track))
  	    {
  	    struct paraFetchData *pfd;
  	    AllocVar(pfd);
  	    pfd->track = track;  // need pointer to be stable
  	    slAddHead(ppfdList, pfd);
  	    track->parallelLoading = TRUE;
  	    }
  	struct track *subtrack;
          for (subtrack=track->subtracks; subtrack; subtrack=subtrack->next)
  	    {
  	    if (isTrackForParallelLoad(subtrack))
  		{
  		if (tdbVisLimitedByAncestors(cart,subtrack->tdb,TRUE,TRUE) != tvHide)
  		    {
  		    struct paraFetchData *pfd;
  		    AllocVar(pfd);
  		    pfd->track = subtrack;  // need pointer to be stable
  		    slAddHead(ppfdList, pfd);
  		    subtrack->parallelLoading = TRUE;
  		    }
  		}
  	    }
  	}
      }
  }
  
  static pthread_mutex_t pfdMutex = PTHREAD_MUTEX_INITIALIZER;
  static struct paraFetchData *pfdList = NULL, *pfdRunning = NULL, *pfdDone = NULL, *pfdNeverStarted = NULL;
  
  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;
  	}
      else
  	{  // move it from the waiting queue to the running queue
  	pfd = slPopHead(&pfdList);
  	slAddHead(&pfdRunning, pfd);
          }
      pthread_mutex_unlock( &pfdMutex );
      if (allDone)
  	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);
- 	checkCollapseEmptySubtracks(pfd->track);
++	checkHideEmptySubtracks(pfd->track);
  	pfd->track->loadItems(pfd->track);
  	pfd->done = TRUE;
  	}
      errCatchEnd(errCatch);
      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
      slAddHead(&pfdDone, pfd);
      pthread_mutex_unlock( &pfdMutex );
  
      }
  }
  
  static int remoteParallelLoadWait(int maxTimeInSeconds)
  /* Wait, checking to see if finished (completed or errAborted).
   * If timed-out or never-ran, record error status.
   * Return error count. */
  {
  int maxTimeInMilliseconds = 1000 * maxTimeInSeconds;
  struct paraFetchData *pfd;
  int errCount = 0;
  int waitTime = 0;
  while(1)
      {
      sleep1000(50); // milliseconds
      waitTime += 50;
      boolean done = TRUE;
      pthread_mutex_lock( &pfdMutex );
      if (pfdList || pfdRunning)
  	done = FALSE;
      pthread_mutex_unlock( &pfdMutex );
      if (done)
          break;
      if (waitTime >= maxTimeInMilliseconds)
          break;
      }
  pthread_mutex_lock( &pfdMutex );
  pfdNeverStarted = pfdList;
  pfdList = NULL;  // stop the workers from starting any more waiting track loads
  for (pfd = pfdNeverStarted; pfd; pfd = pfd->next)
      {
      // track was never even started
      char temp[256];
      safef(temp, sizeof temp, "Ran out of time (%d milliseconds) unable to process  %s", maxTimeInMilliseconds, pfd->track->track);
      pfd->track->networkErrMsg = cloneString(temp);
      ++errCount;
      }
  for (pfd = pfdRunning; pfd; pfd = pfd->next)
      {
      // unfinished track
      char temp[256];
      safef(temp, sizeof temp, "Timeout %d milliseconds exceeded processing %s", maxTimeInMilliseconds, pfd->track->track);
      pfd->track->networkErrMsg = cloneString(temp);
      ++errCount;
      }
  for (pfd = pfdDone; pfd; pfd = pfd->next)
      {
      // some done tracks may have errors
      if (pfd->track->networkErrMsg)
          ++errCount;
      }
  pthread_mutex_unlock( &pfdMutex );
  return errCount;
  }
  
  static int avgLoadTime(struct track *track)
  /* calculate average loadtime across all windows */
  {
  int totalLoadTime = 0;
  int winCount = 0;
  while(track)
      {
      ++winCount;
      totalLoadTime += track->loadTime;
      track = track->nextWindow;
      }
  return (((float)totalLoadTime / winCount) + 0.5);
  }
  
  static int avgDrawTime(struct track *track)
  /* calculate average drawtime across all windows */
  {
  int totalDrawTime = 0;
  int winCount = 0;
  while(track)
      {
      ++winCount;
      totalDrawTime += track->drawTime;
      track = track->nextWindow;
      }
  return (((float)totalDrawTime / winCount) + 0.5);
  }
  
  static int avgWigMafLoadTime(struct track *track)
  /* calculate average wigMaf loadtime across all windows */
  {
  int totalLoadTime = 0;
  int winCount = 0;
  while(track)
      {
      ++winCount;
      if (startsWith("wigMaf", track->tdb->type))
  	if (track->subtracks)
  	    if (track->subtracks->loadTime)
  		totalLoadTime += track->subtracks->loadTime;
      track = track->nextWindow;
      }
  return (((float)totalLoadTime / winCount) + 0.5);
  }
  
  static void printTrackTiming()
  {
  hPrintf("<span class='trackTiming'>track, load time, draw time, total (first window)<br />\n");
  if (virtMode)
      hPrintf("<span class='trackTiming'><idiv style='color:red' >average for all windows in red</idiv><br />\n");
  struct track *track;
  for (track = trackList; track != NULL; track = track->next)
      {
      if (track->visibility == tvHide)
          continue;
      if (trackIsCompositeWithSubtracks(track))  //TODO: Change when tracks->subtracks are always set for composite
          {
          struct track *subtrack;
          for (subtrack = track->subtracks; subtrack != NULL;
               subtrack = subtrack->next)
              if (isSubtrackVisible(subtrack))
  		{
                  hPrintf("%s, %d, %d, %d<br />\n", subtrack->shortLabel,
                              subtrack->loadTime, subtrack->drawTime,
                              subtrack->loadTime + subtrack->drawTime);
  		if (virtMode)
  		    {
  		    int avgLoad = avgLoadTime(subtrack);
  		    int avgDraw = avgDrawTime(subtrack);
  		    hPrintf("<idiv style='color:red' >%s, %d, %d, %d</idiv><br />\n", subtrack->shortLabel,
  				avgLoad, avgDraw,
  				avgLoad + avgDraw);
  		    }
  		}
          }
      else
          {
          hPrintf("%s, %d, %d, %d<br />\n",
  		    track->shortLabel, track->loadTime, track->drawTime,
  		    track->loadTime + track->drawTime);
          if (startsWith("wigMaf", track->tdb->type))
              if (track->subtracks)
                  if (track->subtracks->loadTime)
                      hPrintf("&nbsp; &nbsp; %s wiggle, load %d<br />\n",
                                  track->shortLabel, track->subtracks->loadTime);
          }
  	if (virtMode)
  	    {
  	    int avgLoad = avgLoadTime(track);
  	    int avgDraw = avgDrawTime(track);
  	    hPrintf("<idiv style='color:red' >%s, %d, %d, %d</idiv><br />\n", track->shortLabel,
  			avgLoad, avgDraw,
  			avgLoad + avgDraw);
  	    int avgWigMafLoad = avgWigMafLoadTime(track);
  	    if (avgWigMafLoad > 0)
  		{
  		hPrintf("<idiv style='color:red' >&nbsp; &nbsp; %s wiggle, load %d</idiv><br />\n",
  			    track->shortLabel, avgWigMafLoad);
  		}
  	    }
      }
  hPrintf("</span>\n");
  }
  
  void initTrackList()
  /* need to init tracklist, sometimes early */
  {
  if (!trackList)
      {
      if (measureTiming)
  	measureTime("Time before getTrackList");
      boolean defaultTracks = cgiVarExists("hgt.reset");
      trackList = getTrackList(&groupList, defaultTracks ? -1 : -2);
      if (measureTiming)
  	measureTime("Time after visibilities");
      makeGlobalTrackHash(trackList);
      }
  }
  
  struct track *getTrackListForOneTrack(char *trackName)
  /* Fetch trackList for a single trackName using hgt.trackNameFilter. */
  {
  struct track *saveTrackList = trackList;
  struct group *saveGroupList = groupList;
  char *saveTrackNameFilter = cloneString(cartOptionalString(cart, "hgt.trackNameFilter"));
  // This is an attempt to both get around the limitation imposed by ajax callback hgt.trackNameFilter,
  //  and also to try to optimize it a little so that callbacks only have to load a trackList containing
  //  only the emGeneTable.
  cartSetString(cart, "hgt.trackNameFilter", trackName);
  initTrackList(); // initialize trackList early if needed
  struct track *returnTrackList = trackList;
  cartRemove(cart, "hgt.trackNameFilter");
  // restore
  if (saveTrackNameFilter)
      {
      cartSetString(cart, "hgt.trackNameFilter", saveTrackNameFilter);
      }
  trackList = saveTrackList;
  groupList = saveGroupList;
  return returnTrackList;
  }
  
  void doNextPrevItem(boolean goNext, char *trackName)
  /* In case a next item arrow was clicked on a track, change */
  /* position (i.e. winStart, winEnd, etc.) based on what track it was */
  {
  // create new trackList with just trackName
  struct track *myTrackList = getTrackListForOneTrack(trackName);
  struct track *track = trackFindByName(myTrackList, trackName);
  if ((track != NULL) && (track->nextPrevItem != NULL))
      {
      // custom track big* tracks have pre-opened handle which we should not use
      // because that same bbiFile will get used later in the full track list
      track->bbiFile = NULL;
      track->nextPrevItem(track, goNext);
      }
  }
  
  
  void findBestEMGeneTable(struct track *myTrackList)
  /* Find the best gene table to use for exonMostly and geneMostly. */
  {
  // TODO add support for choosing any gene table of type genePred, genePreExt, bigGenePred
  
  // TODO add support for assembly hubs
  if (trackHubDatabase(database)) // assembly hub? not supported yet
      return; // any table-name matches might just be coincidence in an assembly hub
              // although the hub_ prefix on the track name would help prevent name collisions.
  
  char *orderedTables[] =
  {"knownGene", "refGene", "ensGene",
   "flybaseGene", "sangerGene", "augustusGene", "genscan"};
  int i, len;
  for(i=0, len=ArraySize(orderedTables); i <len; ++i)
      {
      char *table = orderedTables[i];
      emGeneTrack = rFindTrackWithTable(table, myTrackList);
      if (emGeneTrack)
  	{
  	emGeneTable = table;
  	cartSetString(cart, "emGeneTable", emGeneTable);
  	break;
  	}
      }
  }
  
  void setEMGeneTrack()
  /* Find the track for the gene table to use for exonMostly and geneMostly. */
  {
  if (emGeneTable) // we already have it!
      return;
  if (trackHubDatabase(database)) // assembly hub? not supported yet
      return;
  emGeneTable = cloneString(cartOptionalString(cart, "emGeneTable"));
  if (emGeneTable)
      {
      struct track *myTrackList = getTrackListForOneTrack(emGeneTable);
      emGeneTrack = rFindTrackWithTable(emGeneTable, myTrackList);
      }
  if (!emGeneTable || !emGeneTrack)
      {
      cartRemove(cart, "emGeneTable");
      // It is preferable not to create a complete track list early on,
      //  but now we need one to find the best default emGeneTable and track.
      initTrackList();
      findBestEMGeneTable(trackList);
      }
  }
  
  boolean windowsHaveMultipleChroms()
  /* Are there multiple different chromosomes in the windows list? */
  {
  struct window *window;
  for (window=windows->next; window; window=window->next)
      {
      if (!sameString(window->chromName,windows->chromName))
  	return TRUE;
      }
  return FALSE;
  }
  
  static void setSharedLimitedVisAcrossWindows(struct track *track)
  /* Look for lowest limitedVis across all windows
   * if found, set all windows to same lowest limited vis. */
  {
  enum trackVisibility sharedVis = 99;
  struct track *tg;
  for (tg=track; tg; tg=tg->nextWindow)
      {
      if (tg->limitedVisSet)
  	{
  	if (tg->limitedVis < sharedVis)
  	    sharedVis = tg->limitedVis;
  	}
      }
  if (sharedVis != 99)
      {
      for (tg=track; tg; tg=tg->nextWindow)
  	{
  	tg->limitedVis = sharedVis;
  	tg->limitedVisSet = TRUE;
  	}
      }
  }
  
  static void setSharedErrorsAcrossWindows(struct track *track)
  /* Look for network errors across all windows
   * if found, set all windows to same errMsg and set bigWarn track handlers. */
  {
  char *sharedErrMsg = NULL;
  struct track *tg;
  for (tg=track; tg; tg=tg->nextWindow)
      {
      if (!sharedErrMsg && tg->networkErrMsg)
  	{
  	sharedErrMsg = tg->networkErrMsg;
  	break;
  	}
      }
  if (sharedErrMsg)
      {
      for (tg=track; tg; tg=tg->nextWindow)
  	{
  	tg->networkErrMsg = sharedErrMsg;
  	tg->drawItems = bigDrawWarning;
  	tg->totalHeight = bigWarnTotalHeight;
  	}
      }
  }
  
  
  void outCollectionsToJson()
  /* Output the current collections to the hgTracks JSON block. */
  {
  struct grp *groupList = NULL;
  char buffer[4096];
  safef(buffer, sizeof buffer, "%s-%s", customCompositeCartName, database);
  char *hubFile = cartOptionalString(cart, buffer);
  
  if (hubFile != NULL)
      {
      char *hubName = hubNameFromUrl(hubFile);
      struct trackDb *hubTdbs = hubCollectTracks( database,  &groupList);
      struct trackDb *tdb;
      struct jsonElement *jsonList = NULL;
      for(tdb = hubTdbs; tdb;  tdb = tdb->next)
          {
          if (sameString(tdb->grp, hubName))
              {
              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);
      }
  }
  
  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);
  zoomedToBaseLevel = (virtWinBaseCount <= fullInsideWidth / tl.mWidth);
  zoomedToCodonLevel = (ceil(virtWinBaseCount/3) * tl.mWidth) <= fullInsideWidth;
  zoomedToCdsColorLevel = (virtWinBaseCount <= fullInsideWidth*3);
  
  if (psOutput != NULL)
     {
     hPrintDisable();
     hideControls = TRUE;
     withNextItemArrows = FALSE;
     withNextExonArrows = FALSE;
     hgFindMatches = NULL;
     }
  
  /* Tell browser where to go when they click on image. */
  hPrintf("<FORM ACTION=\"%s\" NAME=\"TrackHeaderForm\" id=\"TrackHeaderForm\" METHOD=\"GET\">\n\n", hgTracksName());
  jsonObjectAdd(jsonForClient, "insideX", newJsonNumber(insideX)); // TODO GALT  fullInsideX? or does not matter?
  jsonObjectAdd(jsonForClient, "revCmplDisp", newJsonBoolean(revCmplDisp));
  
  if (hPrintStatus()) cartSaveSession(cart);
  
  /* See if want to include sequence search results. */
  userSeqString = cartOptionalString(cart, "ss");
  if (userSeqString && !ssFilesExist(userSeqString))
      {
      userSeqString = NULL;
      cartRemove(cart, "ss");
      }
  if (!hideControls)
      hideControls = cartUsualBoolean(cart, "hideControls", FALSE);
  
  initTrackList();
  //warn("slCount(trackList) after getTrackList: %d", slCount(trackList));
  /* Tell tracks to load their items. */
  
  
  // honor defaultImgOrder
  if (cgiVarExists("hgt.defaultImgOrder"))
      {
      char wildCard[32];
      safef(wildCard,sizeof(wildCard),"*_%s",IMG_ORDER_VAR);
      cartRemoveLike(cart, wildCard);
      }
  
  // Subtrack settings must be removed when composite/view settings are updated
  parentChildCartCleanup(trackList,cart,oldVars);
  if (measureTiming)
      measureTime("parentChildCartCleanup");
  
  
  /* Honor hideAll and visAll variables */
  if (hideAll || defaultTracks)
      {
      int vis = (hideAll ? tvHide : -1);
      changeTrackVis(groupList, NULL, vis);
      }
  
  if(!psOutput && !cartUsualBoolean(cart, "hgt.imageV1", FALSE))
      {
  
     // re-establish the enlarged portal
     if (imgBoxPortalDimensions(theImgBox,&virtWinStart,&virtWinEnd,&(tl.picWidth),NULL,NULL,NULL,NULL,NULL))
          {
          virtWinBaseCount = virtWinEnd - virtWinStart;
          fullInsideWidth = tl.picWidth - gfxBorder - fullInsideX;
          }
  
      }
  
  char *jsCommand = cartCgiUsualString(cart, hgtJsCommand, "");
  if (!isEmpty(jsCommand))
     {
     cartRemove(cart, hgtJsCommand);
     jsCommandDispatch(jsCommand, trackList);
     }
  
  
  /* adjust visibility */
  for (track = trackList; track != NULL; track = track->next)
      {
      /* adjust track visibility based on supertrack just before load loop */
      if (tdbIsSuperTrackChild(track->tdb))
          limitSuperTrackVis(track);
  
      /* hide tracks not on any windows chromNames */
      boolean hideIt = TRUE;
      struct window *w;
      for (w = windows; w; w=w->next)
          {
          if (hTrackOnChrom(track->tdb, w->chromName))
              hideIt = FALSE;
          }
      if (hideIt)
          {
          track->limitedVis = tvHide;
          track->limitedVisSet = TRUE;
          }
      }
  
  
  if (sameString(cfgOptionDefault("trackLog", "off"), "on"))
      logTrackVisibilities(cartSessionId(cart), trackList);
  
  
  /////////////////
  
  // NEED TO LOAD ALL WINDOWS NOW
  //
  //   Need to load one window at a time!
  //
  //   The use of the global values for a window
  //   means that differerent threads cannot use different global window values.
  //   Threads must run on just one window value at a time.
  //
  //   Begin by making a copy of the track structure for visible tracks
  //   for all windows.
  
  
  // COPY TRACK STRUCTURES for other windows.
  
  // TODO: due to an issue where some loading code is modifying the visibility
  // of subtracks from hide to visible, I am forced to remove the optimization
  // of cloning ONLY non-hidden tracks and subtracks.  If the offending code
  // can be identified and moved into a step proceding the track cloning,
  // then we can return to that optimization.
  //	if (track->visibility != tvHide)
  //		if (subtrack->visibility != tvHide)
  
  windows->trackList = trackList;  // save current track list in window
  struct window *window;
  for (window=windows; window->next; window=window->next)
      {
      struct track *newTrackList = NULL;
      for (track = trackList; track != NULL; track = track->next)
  	{
          isCompositeInAggregate(track); // allow track to recognize its true self
  	track->nextWindow = NULL;
  	//if (track->visibility != tvHide)  // Unable to use this optimization at present
  	    {
  	    struct track *copy;
  	    AllocVar(copy);
  	    memmove(copy,track,sizeof(struct track));
  	    copy->next = NULL;
  	    copy->bbiFile = NULL;  // bigDataUrl custom tracks have already been opened, will re-open for other windows.
  	    copy->nextWindow = NULL;
  	    copy->prevWindow = track;
  	    slAddHead(&newTrackList, copy);
  	    track->nextWindow = copy;
  
  	    // copy subtracks.
  	    copy->subtracks = NULL;
  	    struct track *subtrack;
  	    for (subtrack = track->subtracks; subtrack != NULL; subtrack = subtrack->next)
  		{
  		//if (subtrack->visibility != tvHide)  // Unable to use this optimization at present
  		    {
  		    struct track *subcopy;
  		    AllocVar(subcopy);
  		    memmove(subcopy,subtrack,sizeof(struct track));
  		    subcopy->next = NULL;
  		    subcopy->bbiFile = NULL;  // bigDataUrl custom tracks have already been opened, will re-open for other windows.
  		    subcopy->nextWindow = NULL;
  		    subcopy->prevWindow = subtrack;
  		    slAddHead(&copy->subtracks, subcopy);
  		    subtrack->nextWindow = subcopy;
  		    }
  		}
  	    slReverse(&copy->subtracks);
  	    }
  	}
      slReverse(&newTrackList);
      trackList = newTrackList;
      window->next->trackList = trackList;  // save new track list in window
      }
  trackList = windows->trackList;  // restore original track list
  
  // Loop over each window loading all tracks
  trackLoadingInProgress = TRUE;
  
  // LOAD OPTIMIZATION HACK GALT
  // This is an attempt to try to optimize loading by having multiple regions
  // treated as a single span.  The hack just grabs the dimensions of the first and last windows
  // and uses the loader in the first window to load them, then copies the results to all tracks.
  // This basically has only been tried on BED-like tracks, and only for exon/gene-mostly vmodes.
  // I am not re-partitioning the results after the load, so this means all windows see all items.
  // The reason that tends to work is that by luck most BED handlers have code to check if the item
  // overlaps the current window and to skip it if it does not.
  // I do not expect something so simple would work with wigs and other track types.
  // Even if we do want to optimize the BED-like tracks (which are already the fastest loading type),
  // to handle all of the virtmodes properly, this would have be be done differently.
  // Instead of just lumping them all into a single range, you would have to cluster together
  // ranges that are close together and on the same chromosome.
  // Clearly this was just to test an idea for optimizing.
  // NOT FINISHED.
  bool loadHack = FALSE; //TRUE;  // probably should only be tried on non-wiggle tracks
  //warn ("loadHack = %d", loadHack); // TODO
  int lastWinEnd = 0;
  for (window=windows; window; window=window->next)
      lastWinEnd = window->winEnd;
  
  for (window=windows; window; window=window->next)
      {
      trackList = window->trackList;  // set track list
      setGlobalsFromWindow(window);
  
      // TEMP HACK GALT REMOVE
      if (loadHack)
  	{
  	if (currentWindow == windows) // first window
  	    winEnd = lastWinEnd; // so now we load the entire span inside the first window.
  	}
  
      /* pre-load remote tracks in parallel */
      int ptMax = atoi(cfgOptionDefault("parallelFetch.threads", "20"));  // default number of threads for parallel fetch.
      int pfdListCount = 0;
      pthread_t *threads = NULL;
      if (ptMax > 0)     // parallelFetch.threads=0 to disable parallel fetch
  	{
  	findLeavesForParallelLoad(trackList, &pfdList);
  	pfdListCount = slCount(pfdList);
  	/* launch parallel threads */
  	ptMax = min(ptMax, pfdListCount);
  	if (ptMax > 0)
  	    {
  	    AllocArray(threads, ptMax);
  	    /* Create threads */
  	    int pt;
  	    for (pt = 0; pt < ptMax; ++pt)
  		{
  		int rc = pthread_create(&threads[pt], NULL, remoteParallelLoad, &threads[pt]);
  		if (rc)
  		    {
  		    errAbort("Unexpected error %d from pthread_create(): %s",rc,strerror(rc));
  		    }
  		}
  	    }
  	}
  
      // TODO GALT
      /* load regular tracks */
      for (track = trackList; track != NULL; track = track->next)
  	{
  	if (track->visibility != tvHide)
  	    {
  	    if (!track->parallelLoading)
  		{
  		if (measureTiming)
  		    lastTime = clock1000();
  
  		checkMaxWindowToDraw(track);
  
- 		checkCollapseEmptySubtracks(track);     // TODO: Test with multi-window feature
++		checkHideEmptySubtracks(track);     // TODO: Test with multi-window feature
 +
  		checkIfWiggling(cart, track);
  
  		if (!loadHack)
  		    {
  		    track->loadItems(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;
  			track->height = track->prevWindow->height;
  			track->lineHeight = track->prevWindow->lineHeight;
  			track->heightPer = track->prevWindow->heightPer;
  			// TODO does this work for subtracks or parents/children?
  			}
  		    }
  
  		if (measureTiming)
  		    {
  		    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)
  	{
  	setSharedLimitedVisAcrossWindows(sub);
  	}
      }
  
  // Look for network errors across all windows
  // if found, set all windows to same errMsg and set bigWarn track handlers.
  for (track = trackList; track != NULL; track = track->next)
      {
      setSharedErrorsAcrossWindows(track);
      struct track *sub;
      for (sub=track->subtracks; sub; sub=sub->next)
  	{
  	setSharedErrorsAcrossWindows(sub);
  	}
      }
  
  
  
  //////////////  END OF MULTI-WINDOW LOOP
  
  
  printTrackInitJavascript(trackList);
  
  /* Generate two lists of hidden variables for track group visibility.  Kludgy,
     but required b/c we have two different navigation forms on this page, but
     we want open/close changes in the bottom form to be submitted even if the user
     submits via the top form. */
  struct dyString *trackGroupsHidden1 = newDyString(1000);
  struct dyString *trackGroupsHidden2 = newDyString(1000);
  for (group = groupList; group != NULL; group = group->next)
      {
      if (group->trackList != NULL)
          {
          int looper;
          for (looper=1;looper<=2;looper++)
              {
              boolean isOpen = !isCollapsedGroup(group);
              char buf[1000];
              safef(buf, sizeof(buf), "<input type='hidden' name=\"%s\" id=\"%s_%d\" value=\"%s\">\n",
  		collapseGroupVar(group->name), collapseGroupVar(group->name), looper, isOpen ? "0" : "1");
              dyStringAppend(looper == 1 ? trackGroupsHidden1 : trackGroupsHidden2, buf);
              }
          }
      }
  
  if (theImgBox)
      {
      // If a portal was established, then set the global dimensions back to the portal size
      if (imgBoxPortalDimensions(theImgBox,NULL,NULL,NULL,NULL,&virtWinStart,&virtWinEnd,&(tl.picWidth),NULL))
          {
          virtWinBaseCount = virtWinEnd - virtWinStart;
          fullInsideWidth = tl.picWidth-gfxBorder-fullInsideX;
          }
      }
  /* Center everything from now on. */
  hPrintf("<CENTER>\n");
  
  outCollectionsToJson();
  
  jsonObjectAdd(jsonForClient, "winStart", newJsonNumber(virtWinStart));
  jsonObjectAdd(jsonForClient, "winEnd", newJsonNumber(virtWinEnd));
  jsonObjectAdd(jsonForClient, "chromName", newJsonString(virtChromName));
  
  // Tell javascript about multiple windows info
  if (virtMode)
      {
      // pre windows
      long preVWinStart = virtWinStart - virtWinBaseCount;
      if (preVWinStart < 0)
  	preVWinStart = 0;
      long preVWinEnd = virtWinStart;
      struct window *preWindows = makeWindowListFromVirtChrom(preVWinStart, preVWinEnd);
      struct jsonElement *jsonForList = newJsonList(NULL);
      for(window=preWindows;window;window=window->next)
  	{
  	struct jsonElement *jsonForWindow = NULL;
  	jsonForWindow = newJsonObject(newHash(8));
  	jsonObjectAdd(jsonForWindow, "chromName",   newJsonString(window->chromName));
  	jsonObjectAdd(jsonForWindow, "winStart",    newJsonNumber(window->winStart));
  	jsonObjectAdd(jsonForWindow, "winEnd",      newJsonNumber(window->winEnd));
  	jsonObjectAdd(jsonForWindow, "insideX",     newJsonNumber(window->insideX));
  	jsonObjectAdd(jsonForWindow, "insideWidth", newJsonNumber(window->insideWidth));
  	jsonObjectAdd(jsonForWindow, "virtStart",   newJsonNumber(window->virtStart));
  	jsonObjectAdd(jsonForWindow, "virtEnd",     newJsonNumber(window->virtEnd));
  	jsonListAdd(jsonForList, jsonForWindow);
  	}
      slReverse(&jsonForList->val.jeList);
      jsonObjectAdd(jsonForClient, "windowsBefore", jsonForList);
  
      jsonForList = newJsonList(NULL);
      for(window=windows;window;window=window->next)
  	{
  	struct jsonElement *jsonForWindow = NULL;
  	jsonForWindow = newJsonObject(newHash(8));
  	jsonObjectAdd(jsonForWindow, "chromName",   newJsonString(window->chromName));
  	jsonObjectAdd(jsonForWindow, "winStart",    newJsonNumber(window->winStart));
  	jsonObjectAdd(jsonForWindow, "winEnd",      newJsonNumber(window->winEnd));
  	jsonObjectAdd(jsonForWindow, "insideX",     newJsonNumber(window->insideX));
  	jsonObjectAdd(jsonForWindow, "insideWidth", newJsonNumber(window->insideWidth));
  	jsonObjectAdd(jsonForWindow, "virtStart",   newJsonNumber(window->virtStart));
  	jsonObjectAdd(jsonForWindow, "virtEnd",     newJsonNumber(window->virtEnd));
  	jsonListAdd(jsonForList, jsonForWindow);
  	}
      slReverse(&jsonForList->val.jeList);
      jsonObjectAdd(jsonForClient, "windows", jsonForList);
  
      // post windows
      long postVWinStart = virtWinEnd;
      long postVWinEnd = virtWinEnd + virtWinBaseCount;
      if (postVWinEnd > virtSeqBaseCount)
  	postVWinEnd = virtSeqBaseCount;
      struct window *postWindows = makeWindowListFromVirtChrom(postVWinStart, postVWinEnd);
      jsonForList = newJsonList(NULL);
      for(window=postWindows;window;window=window->next)
  	{
  	struct jsonElement *jsonForWindow = NULL;
  	jsonForWindow = newJsonObject(newHash(8));
  	jsonObjectAdd(jsonForWindow, "chromName",   newJsonString(window->chromName));
  	jsonObjectAdd(jsonForWindow, "winStart",    newJsonNumber(window->winStart));
  	jsonObjectAdd(jsonForWindow, "winEnd",      newJsonNumber(window->winEnd));
  	jsonObjectAdd(jsonForWindow, "insideX",     newJsonNumber(window->insideX));
  	jsonObjectAdd(jsonForWindow, "insideWidth", newJsonNumber(window->insideWidth));
  	jsonObjectAdd(jsonForWindow, "virtStart",   newJsonNumber(window->virtStart));
  	jsonObjectAdd(jsonForWindow, "virtEnd",     newJsonNumber(window->virtEnd));
  	jsonListAdd(jsonForList, jsonForWindow);
  	}
      slReverse(&jsonForList->val.jeList);
      jsonObjectAdd(jsonForClient, "windowsAfter", jsonForList);
  
      jsonForList = newJsonList(NULL);
      // also store js nonVirtPosition
      jsonObjectAdd(jsonForClient, "nonVirtPosition", newJsonString(cartString(cart, "nonVirtPosition")));
      jsonObjectAdd(jsonForClient, "virtChromChanged", newJsonBoolean(virtChromChanged));
      jsonObjectAdd(jsonForClient, "virtualSingleChrom", newJsonBoolean(virtualSingleChrom())); // DISGUISE POS
      }
  
  char dbPosKey[256];
  safef(dbPosKey, sizeof(dbPosKey), "position.%s", database);
  jsonObjectAdd(jsonForClient, "lastDbPos", newJsonString(cartString(cart, dbPosKey)));
  
  // hide chromIdeo
  if ((trackImgOnly && !ideogramToo)
  || (sameString(virtModeType, "customUrl") && windowsHaveMultipleChroms()) // Special case hide by request
  )
      {
      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)
      {
      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 . */
      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");
  
      /* 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",
                         "return imageV2.navigateButtonClick(this);");
      hButtonWithOnClick("hgt.right3", ">>>", "move 95% to the right",
                         "return imageV2.navigateButtonClick(this);");
      hWrites(" zoom in ");
      /* use button maker that determines padding, so we can share constants */
      topButton("hgt.in1", ZOOM_1PT5X);
      topButton("hgt.in2", ZOOM_3X);
      topButton("hgt.in3", ZOOM_10X);
      topButton("hgt.inBase", ZOOM_BASE);
      hWrites(" zoom out ");
      topButton("hgt.out1", ZOOM_1PT5X);
      topButton("hgt.out2", ZOOM_3X);
      topButton("hgt.out3", ZOOM_10X);
      topButton("hgt.out4", ZOOM_100X);
      hWrites("<div style='height:0.3em;'></div>\n");
  #endif//ndef USE_NAVIGATION_LINKS
  
      if (showTrackControls)
          {
  	/* Break into a second form so that zooming and scrolling
  	 * can be done with a 'GET' so that user can back up from details
  	 * page without Internet Explorer popping up an annoying dialog.
  	 * Do rest of page as a 'POST' so that the ultra-long URL from
  	 * all the track controls doesn't break things.  IE URL limit
  	 * is 2000 bytes, but some firewalls impose a ~1000 byte limit.
  	 * As a side effect of breaking up the page into two forms
  	 * we need to repeat the position in a hidden variable here
  	 * so that zoom/scrolling always has current position to work
  	 * from. */
          // This 'dirty' field is used to check if js/ajax changes to the page have occurred.
          // If so and it is reached by the back button, a page reload will occur instead.
  	char buf[256];
  	if (virtualSingleChrom()) // DISGUISE VMODE
  	    safef(buf, sizeof buf, "%s", windowsSpanPosition());
  	else
  	    safef(buf, sizeof buf, "%s:%ld-%ld", virtChromName, virtWinStart+1, virtWinEnd);
          hPrintf("<INPUT TYPE='text' style='display:none;' name='dirty' id='dirty' VALUE='false'>\n");
          hPrintf("<INPUT TYPE=HIDDEN id='positionHidden' name='position' "
                  "VALUE=\"%s\">", buf);
          hPrintf("\n%s", trackGroupsHidden1->string);
          hPrintf("</CENTER></FORM>\n");
          hPrintf("<FORM ACTION=\"%s\" NAME=\"TrackForm\" id=\"TrackForm\" METHOD=\"POST\">\n\n",
                  hgTracksName());
  	    hPrintf("%s", trackGroupsHidden2->string);
  	    freeDyString(&trackGroupsHidden1);
  	    freeDyString(&trackGroupsHidden2);
  	if (!psOutput) cartSaveSession(cart);   /* Put up hgsid= as hidden variable. */
  	hPrintf("<CENTER>");
  	}
  
  
      /* Make line that says position. */
  	{
  	char buf[256];
  	char *survey = cfgOptionEnv("HGDB_SURVEY", "survey");
  	char *surveyLabel = cfgOptionEnv("HGDB_SURVEY_LABEL", "surveyLabel");
  	    char *javascript = "document.location = '/cgi-bin/hgTracks?db=' + document.TrackForm.db.options[document.TrackForm.db.selectedIndex].value;";
  	    if (containsStringNoCase(database, "zoo"))
  		{
  		hPuts("Organism ");
  		printAssemblyListHtmlExtra(database, "change", javascript);
  		}
  
  	if (virtualSingleChrom()) // DISGUISE VMODE
  	    safef(buf, sizeof buf, "%s", windowsSpanPosition());
  	else
  	    safef(buf, sizeof buf, "%s:%ld-%ld", virtChromName, virtWinStart+1, virtWinEnd);
  	
  	position = cloneString(buf);
- 	hPrintf("<span class='positionDisplay' id='positionDisplay' title='click to copy position to input box'>%s</span>", addCommasToPos(database, position));
+         char *pressedClass = "", *showVirtRegions = "";
+         if (differentString(virtModeType, "default"))
+             {
+             pressedClass = "pressed";
+             showVirtRegions = "show multi-region position ranges and ";
+             }
+ 	hPrintf("<span class='positionDisplay %s' id='positionDisplay' "
+                 "title='click to %s copy position to input box'>%s</span>", 
+                         pressedClass, showVirtRegions, addCommasToPos(database, position));
  	hPrintf("<input type='hidden' name='position' id='position' value='%s'>\n", buf);
  	sprintLongWithCommas(buf, virtWinEnd - virtWinStart);
  	hPrintf(" <span id='size'>%s</span> bp. ", buf);
  	hPrintf("<input class='positionInput' type='text' name='hgt.positionInput' id='positionInput' size='60'>\n");
  	hWrites(" ");
  	hButton("hgt.jump", "go");
  	if (!trackHubDatabase(database))
  	    {
              jsonObjectAdd(jsonForClient, "assemblySupportsGeneSuggest", newJsonBoolean(assemblySupportsGeneSuggest(database)));
              if (assemblySupportsGeneSuggest(database))
                  hPrintf("<input type='hidden' name='hgt.suggestTrack' id='suggestTrack' value='%s'>\n", assemblyGeneSuggestTrack(database));
  	    }
  	if (survey && differentWord(survey, "off"))
              hPrintf("&nbsp;&nbsp;<span style='background-color:yellow;'>"
                      "<A HREF='%s' TARGET=_BLANK><EM><B>%s</EM></B></A></span>\n",
                      survey, surveyLabel ? surveyLabel : "Take survey");
  	hPutc('\n');
  	}
      }
  
  // TODO GALT  how to handle ideos?
  boolean nukeIdeoFromList = FALSE;
  for(window=windows;window;window=window->next)
      {
      setGlobalsFromWindow(window);
  
      if (window == windows) // first window
  	{	
  	/* Make chromosome ideogram gif and map. */
  	nukeIdeoFromList = makeChromIdeoImage(&trackList, psOutput, ideoTn);
  	window->trackList = trackList;  // the variable may have been updated.
  	// TODO make this not just be centered over the entire image,
  	// but rather centered over the individual chromosome.
  	// notice that it modifies trackList, and visibility settings potentially need parallelization for windows
  	}
      else
  	{
  	// TODO should be more than this. But at least this makes the same trackList mods to the other windows.
  	if (nukeIdeoFromList)
  	    {
  	    struct track *ideoTrack = chromIdeoTrack(window->trackList);
  	    if (ideoTrack)
  		{
  		slRemoveEl(&window->trackList, ideoTrack);
  		}
  	    }
  	}
  
      }
  setGlobalsFromWindow(windows); // first window // restore globals
  
  #ifdef USE_NAVIGATION_LINKS
  hPrintf("<TABLE BORDER=0 CELLPADDING=0 width='%d'><tr style='font-size:small;'>\n",
          tl.picWidth);//min(tl.picWidth, 800));
  hPrintf("<td width='40' align='left'><a href='?hgt.left3=1' "
          "title='move 95&#37; to the left'>&lt;&lt;&lt;</a>\n");
  hPrintf("<td width='30' align='left'><a href='?hgt.left2=1' "
          "title='move 47.5&#37; to the left'>&lt;&lt;</a>\n");
  hPrintf("<td width='20' align='left'><a href='?hgt.left1=1' "
          "title='move 10&#37; to the left'>&lt;</a>\n");
  
  hPrintf("<td>&nbsp;</td>\n"); // Without width cell expands table with, forcing others to sides
  hPrintf("<td width='40' align='left'><a href='?hgt.in1=1' "
          "title='zoom in 1.5x'>&gt;&nbsp;&lt;</a>\n");
  hPrintf("<td width='60' align='left'><a href='?hgt.in2=1' "
          "title='zoom in 3x'>&gt;&gt;&nbsp;&lt;&lt;</a>\n");
  hPrintf("<td width='80' align='left'><a href='?hgt.in3=1' "
          "title='zoom in 10x'>&gt;&gt;&gt;&nbsp;&lt;&lt;&lt;</a>\n");
  hPrintf("<td width='40' align='left'><a href='?hgt.inBase=1' "
          "title='zoom in to base range'>&gt;<i>base</i>&lt;</a>\n");
  
  hPrintf("<td>&nbsp;</td>\n"); // Without width cell expands table with, forcing others to sides
  hPrintf("<td width='40' align='right'><a href='?hgt.out1=1' "
          "title='zoom out 1.5x'>&lt;&nbsp;&gt;</a>\n");
  hPrintf("<td width='60' align='right'><a href='?hgt.out2=1' "
          "title='zoom out 3x'>&lt;&lt;&nbsp;&gt;&gt;</a>\n");
  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)
      {
      struct controlGrid *cg = NULL;
  
      /* note a trick of WIDTH=27 going on here.  The 6,15,6 widths following
       * go along with this trick */
      hPrintf("<TABLE BORDER=0 CELLSPACING=1 CELLPADDING=1 WIDTH=%d COLS=%d><TR>\n",
              tl.picWidth, 27);
  #ifndef USE_NAVIGATION_LINKS
      hPrintf("<TD COLSPAN=6 ALIGN=left NOWRAP>");
      hPrintf("move start<BR>");
      hButtonWithOnClick("hgt.dinkLL", " < ", "move start position to the left",
                         "return imageV2.navigateButtonClick(this);");
      hTextVar("dinkL", cartUsualString(cart, "dinkL", "2.0"), 3);
      hButtonWithOnClick("hgt.dinkLR", " > ", "move start position to the right",
                         "return imageV2.navigateButtonClick(this);");
      hPrintf("</TD>");
      hPrintf("<td width='30'>&nbsp;</td>\n");
  #endif//ndef USE_NAVIGATION_LINKS
      hPrintf("<TD class='infoText' COLSPAN=15 style=\"white-space:normal\">"); // allow this text to wrap
      hWrites("Click on a feature for details. ");
      hWrites("Click or drag in the base position track to zoom in. ");
      hWrites("Click side bars for track options. ");
      hWrites("Drag side bars or labels up or down to reorder tracks. ");
      hWrites("Drag tracks left or right to new position. ");
      hWrites("Press \"?\" for keyboard shortcuts. ");
      hPrintf("</TD>");
  #ifndef USE_NAVIGATION_LINKS
      hPrintf("<td width='30'>&nbsp;</td>\n");
      hPrintf("<TD COLSPAN=6 ALIGN=right NOWRAP>");
      hPrintf("move end<BR>");
      hButtonWithOnClick("hgt.dinkRL", " < ", "move end position to the left",
                         "return imageV2.navigateButtonClick(this);");
      hTextVar("dinkR", cartUsualString(cart, "dinkR", "2.0"), 3);
      hButtonWithOnClick("hgt.dinkRR", " > ", "move end position to the right",
                         "return imageV2.navigateButtonClick(this);");
      hPrintf("</TD>");
  #endif//ndef USE_NAVIGATION_LINKS
      hPrintf("</TR></TABLE>\n");
  
      /* Display bottom control panel. */
      if (isSearchTracksSupported(database,cart))
          {
          cgiMakeButtonWithMsg(TRACK_SEARCH, TRACK_SEARCH_BUTTON,TRACK_SEARCH_HINT);
          hPrintf(" ");
          }
      hButtonWithMsg("hgt.reset", "default tracks","Display only default tracks");
      hPrintf("&nbsp;");
      hButtonWithMsg("hgt.defaultImgOrder", "default order",
                     "Display current tracks in their default order");
      // if (showTrackControls)  - always show "hide all", Hiram 2008-06-26
          {
          hPrintf("&nbsp;");
          hButtonWithMsg("hgt.hideAll", "hide all","Hide all currently visibile tracks");
          }
  
      hPrintf(" ");
      hPrintf("<INPUT TYPE='button' id='ct_add' VALUE='%s' title='%s'>",
              hasCustomTracks ? CT_MANAGE_BUTTON_LABEL : CT_ADD_BUTTON_LABEL,
              hasCustomTracks ? "Manage your custom tracks" : "Add your own custom tracks");
      jsOnEventById("click", "ct_add", "document.customTrackForm.submit();return false;");
  
      hPrintf(" ");
      if (hubConnectTableExists())
          {
          hPrintf("<INPUT TYPE='button' id='th_form' VALUE='track hubs'"
                  "return false;' title='Import tracks from hubs'>");
  	jsOnEventById("click", "th_form", "document.trackHubForm.submit();");
          hPrintf(" ");
          }
  
      hButtonWithMsg("hgTracksConfigPage", "configure","Configure image and track selection");
      hPrintf(" ");
  
-     hButtonWithOnClick("hgTracksConfigMultiRegionPage",
- 	"multi-region", "Configure multi-region display options", "popUpHgt.hgTracks('multi-region config'); return false;");
+     hButtonMaybePressed("hgTracksConfigMultiRegionPage", "multi-region", 
+                         "Configure multi-region display options", 
+                         "popUpHgt.hgTracks('multi-region config'); return false;", virtMode);
      hPrintf(" ");
  
      if (!hIsGsidServer())
          {
-         hButtonWithMsg("hgt.toggleRevCmplDisp", "reverse",
+         hButtonMaybePressed("hgt.toggleRevCmplDisp", "reverse",
                                 revCmplDisp ? "Show forward strand at this location"
-                                    : "Show reverse strand at this location");
+                                            : "Show reverse strand at this location",
+                                NULL, revCmplDisp);
          hPrintf(" ");
          }
  
      hButtonWithOnClick("hgt.setWidth", "resize", "Resize image width to browser window size", "hgTracksSetWidth()");
      hPrintf(" ");
  
      hButtonWithMsg("hgt.refresh", "refresh","Refresh image");
  
      hPrintf("<BR>\n");
  
      if( chromosomeColorsMade )
          {
          hPrintf("<B>Chromosome Color Key:</B><BR> ");
          hPrintf("<IMG SRC = \"../images/new_colorchrom.gif\" BORDER=1 WIDTH=596 HEIGHT=18 ><BR>\n");
          }
  
      if (showTrackControls)
  	{
  	/* Display viewing options for each track. */
          /* Chuck: This is going to be wrapped in a table so that
           * the controls don't wrap around randomly */
          hPrintf("<table border=0 cellspacing=1 cellpadding=1 width=%d>\n", CONTROL_TABLE_WIDTH);
          hPrintf("<tr><td align='left'>\n");
  
          hButtonWithOnClick("hgt.collapseGroups", "collapse all", "collapse all track groups",
                             "return vis.expandAllGroups(false)");
          hPrintf("</td>");
  
          hPrintf("<td colspan='%d' class='infoText' align='CENTER' nowrap>"
                  "Use drop-down controls below and press refresh to alter tracks "
                  "displayed.<BR>"
                  "Tracks with lots of items will automatically be displayed in "
                  "more compact modes.</td>\n", MAX_CONTROL_COLUMNS - 2);
  
          hPrintf("<td align='right'>");
          hButtonWithOnClick("hgt.expandGroups", "expand all", "expand all track groups",
                             "return vis.expandAllGroups(true)");
          hPrintf("</td></tr>");
  
          if (!hIsGsidServer())
  	    {
  	    cg = startControlGrid(MAX_CONTROL_COLUMNS, "left");
  	    }
  	else
  	    {
  	    /* 4 cols fit GSID's display better */
  	    cg = startControlGrid(4, "left");
  	    }
  	for (group = groupList; group != NULL; group = group->next)
  	    {
  	    if (group->trackList == NULL)
  		continue;
  
  	    struct trackRef *tr;
  
  	    /* check if group section should be displayed */
  	    char *otherState;
  	    char *indicator;
  	    char *indicatorImg;
  	    boolean isOpen = !isCollapsedGroup(group);
  	    collapseGroupGoodies(isOpen, TRUE, &indicatorImg,
  				    &indicator, &otherState);
  	    hPrintf("<TR>");
  	    cg->rowOpen = TRUE;
              if (!hIsGsidServer())
                  hPrintf("<th align=\"left\" colspan=%d class='blueToggleBar'>",MAX_CONTROL_COLUMNS);
              else
                  hPrintf("<th align=\"left\" colspan=%d class='blueToggleBar'>",
                          MAX_CONTROL_COLUMNS-1);
  
              hPrintf("<table style='width:100%%;'><tr><td style='text-align:left;'>");
              hPrintf("\n<A NAME=\"%sGroup\"></A>",group->name);
  
  	    char idText[256];
  	    safef(idText, sizeof idText, "%s_button", group->name);
              hPrintf("<IMG class='toggleButton'"
                      " id='%s' src=\"%s\" alt=\"%s\" title='%s this group'>&nbsp;&nbsp;",
                      idText, indicatorImg, indicator,isOpen?"Collapse":"Expand");
  	    jsOnEventByIdF("click", idText, "return vis.toggleForGroup(this, '%s');", group->name);
  
              if (isHubTrack(group->name))
  		{
                  if (strstr(group->label, "Collections"))
                      {
                      safef(idText, sizeof idText, "%s_edit", group->name);
                      hPrintf("<input name=\"hubEditButton\" id='%s'"
                          " type=\"button\" value=\"edit\">\n", idText);
                      jsOnEventByIdF("click", idText,
                          "document.editHubForm.submit();return true;");
                      }
                  }
  
              hPrintf("</td><td style='text-align:center; width:90%%;'>\n<B>%s</B>", group->label);
              hPrintf("</td><td style='text-align:right;'>\n");
              if (isHubTrack(group->name))
  		{
  		safef(idText, sizeof idText, "%s_disconn", group->name);
                  hPrintf("<input name=\"hubDisconnectButton\" id='%s'"
                      " type=\"button\" value=\"disconnect\">\n", idText);
  		jsOnEventByIdF("click", idText,
                      "document.disconnectHubForm.elements['hubId'].value='%s';"
                      "document.disconnectHubForm.submit();return true;",
  		    group->name + strlen(hubTrackPrefix));
  		}
  
              hPrintf("<input type='submit' name='hgt.refresh' value='refresh' "
                      "title='Update image with your changes'>\n");
              hPrintf("</td></tr></table></th>\n");
              controlGridEndRow(cg);
  
              /* Base Position track goes into map group, which will always exist. */
              if (!showedRuler && sameString(group->name, "map") )
  		{
  		char *url = trackUrl(RULER_TRACK_NAME, chromName);
  		showedRuler = TRUE;
  		myControlGridStartCell(cg, isOpen, group->name);
  		hPrintf("<A HREF=\"%s\">", url);
  		hPrintf(" %s<BR> ", RULER_TRACK_LABEL);
  		hPrintf("</A>");
  		hDropListClassWithStyle("ruler", rulerMenu,
  			sizeof(rulerMenu)/sizeof(char *), rulerMenu[rulerMode],
  			rulerMode == tvHide ? "hiddenText" : "normalText",
  			TV_DROPDOWN_STYLE);
  		controlGridEndCell(cg);
  		freeMem(url);
  		}
  
  	    /* Add supertracks to  track list, sort by priority and
  	     * determine if they have visible member tracks */
  	    groupTrackListAddSuper(cart, group);
  
  	    /* Display track controls */
  	    for (tr = group->trackList; tr != NULL; tr = tr->next)
  		{
  		struct track *track = tr->track;
  		if (tdbIsSuperTrackChild(track->tdb))
  		    /* don't display supertrack members */
  		    continue;
  		myControlGridStartCell(cg, isOpen, group->name);
  		if (track->hasUi)
  		    {
  		    char *url = trackUrl(track->track, chromName);
  		    char *longLabel = replaceChars(track->longLabel, "\"", "&quot;");
                      hPrintPennantIcon(track->tdb);
  
                      // Print an icon before the title when one is defined
                      hPrintf("<A HREF=\"%s\" title=\"%s\">", url, longLabel);
  
                      freeMem(url);
                      freeMem(longLabel);
                      }
  		hPrintf(" %s", track->shortLabel);
  		if (tdbIsSuper(track->tdb))
  		    hPrintf("...");
  		hPrintf("<BR> ");
  		if (track->hasUi)
  		    hPrintf("</A>");
  
  		if (hTrackOnChrom(track->tdb, chromName))
  		    {
  		    if (tdbIsSuper(track->tdb))
  			superTrackDropDown(cart, track->tdb,
  					    superTrackHasVisibleMembers(track->tdb));
  		    else
                          {
                          /* check for option of limiting visibility to one mode */
                          hTvDropDownClassVisOnly(track->track, track->visibility,
                                                  rTdbTreeCanPack(track->tdb),
                                                  (track->visibility == tvHide) ? "hiddenText"
                                                                                : "normalText",
                                                  trackDbSetting(track->tdb, "onlyVisibility"));
                          }
                      }
  		else
  		    /* If track is not on this chrom print an informational
  		    message for the user. */
  		    hPrintf("[No data-%s]", chromName);
  		controlGridEndCell(cg);
  		}
  	    /* now finish out the table */
  	    if (group->next != NULL)
  		controlGridEndRow(cg);
  	    }
  	endControlGrid(&cg);
  	}
  
      if (measureTiming)
          printTrackTiming();
  
      hPrintf("</DIV>\n");
      }
  if (showTrackControls)
      hButton("hgt.refresh", "refresh");
  hPrintf("</CENTER>\n");
  
  #ifdef SLOW
  /* We'll rely on the end of program to do the cleanup.
   * It turns out that the 'free' routine on Linux is
   * quite slow.  For chromosome level views the browser
   * spends about 1/3 of it's time doing the cleanup
   * below if it's enabled.  Since we really don't
   * need to reclaim this memory at this point I'm
   * taking this out.  Please don't delete the code though.
   * I'll like to keep it for testing now and then. -jk. */
  
  // TODO GALT cleanup sibs too? probably can do for window copies but low priority.
  /* Clean up. */
  for (track = trackList; track != NULL; track = track->next)
      {
      if (track->visibility != tvHide)
  	{
  	if (track->freeItems != NULL)
  	    track->freeItems(track);
  	lmCleanup(&track->lm);
  	}
      }
  #endif /* SLOW */
  hPrintf("</FORM>\n");
  
  /* hidden form for custom tracks CGI */
  hPrintf("<FORM ACTION='%s' NAME='customTrackForm'>", hgCustomName());
  cartSaveSession(cart);
  hPrintf("</FORM>\n");
  
  /* hidden form for composite builder CGI */
  hPrintf("<FORM ACTION='%s' NAME='editHubForm'>", hgCollectionName());
  cartSaveSession(cart);
  hPrintf("</FORM>\n");
  
  /* hidden form for track hub CGI */
  hPrintf("<FORM ACTION='%s' NAME='trackHubForm'>", hgHubConnectName());
  cartSaveSession(cart);
  hPrintf("</FORM>\n");
  
  // this is the form for the disconnect hub button
  hPrintf("<FORM ACTION=\"%s\" NAME=\"disconnectHubForm\">\n",  "../cgi-bin/hgTracks");
  cgiMakeHiddenVar("hubId", "");
  cgiMakeHiddenVar(hgHubDoDisconnect, "on");
  cgiMakeHiddenVar(hgHubConnectRemakeTrackHub, "on");
  cartSaveSession(cart);
  puts("</FORM>");
  
  // TODO GALT nothing to do here.
  pruneRedundantCartVis(trackList);
  if (measureTiming)
      measureTime("Done with trackForm");
  }
  
  static void toggleRevCmplDisp()
  /* toggle the reverse complement display mode */
  {
  // forces complement bases to match display
  revCmplDisp = !revCmplDisp;
  cartSetBooleanDb(cart, database, REV_CMPL_DISP, revCmplDisp);
  cartSetBooleanDb(cart, database, COMPLEMENT_BASES_VAR, revCmplDisp);
  }
  
  void zoomToSize(long newSize)
  /* Zoom so that center stays in same place,
   * but window is new size.  If necessary move
   * center a little bit to keep it from going past
   * edges. */
  {
  long center = ((long long)virtWinStart + (long long)virtWinEnd)/2;
  if (center < 0)
      errAbort("zoomToSize: error computing center: %ld = (%ld + %ld)/2\n",
               center, virtWinStart, virtWinEnd);
  if (newSize > virtSeqBaseCount)
      newSize = virtSeqBaseCount;
  virtWinStart = center - newSize/2;
  virtWinEnd = virtWinStart + newSize;
  if (virtWinStart <= 0)
      {
      virtWinStart = 0;
      virtWinEnd = newSize;
      }
  else if (virtWinEnd > virtSeqBaseCount)
      {
      virtWinEnd = virtSeqBaseCount;
      virtWinStart = virtWinEnd - newSize;
      }
  virtWinBaseCount = virtWinEnd - virtWinStart;
  }
  
  void zoomAroundCenter(double amount)
  /* Set ends so as to zoom around center by scaling amount. */
  {
  double newSizeDbl = (virtWinBaseCount*amount + 0.5);
  long newSize;
  if (newSizeDbl > virtSeqBaseCount)
      newSize = virtSeqBaseCount;
  else if (newSizeDbl < 1.0)
      newSize = 1;
  else
      newSize = (long)newSizeDbl;
  zoomToSize(newSize);
  }
  
  void zoomToBaseLevel()
  /* Set things so that it's zoomed to base level. */
  {
  zoomToSize(fullInsideWidth/tl.mWidth);
  if (rulerMode == tvHide)
      cartSetString(cart, "ruler", "dense");
  }
  
  void relativeScroll(double amount)
  /* Scroll percentage of visible window. */
  {
  long offset;
  long newStart, newEnd;
  if (revCmplDisp)
      amount = -amount;
  offset = (long)(amount * virtWinBaseCount);
  /* Make sure don't scroll of ends. */
  newStart = virtWinStart + offset;
  newEnd = virtWinEnd + offset;
  if (newStart < 0)
      offset = -virtWinStart;
  else if (newEnd > virtSeqBaseCount)
      offset = virtSeqBaseCount - virtWinEnd;
  
  /* Move window. */
  virtWinStart += offset;
  virtWinEnd += offset;
  }
  
  void dinkWindow(boolean start, long dinkAmount)
  /* Move one end or other of window a little. */
  {
  if (revCmplDisp)
      {
      start = !start;
      dinkAmount = -dinkAmount;
      }
  if (start)
      {
      virtWinStart += dinkAmount;
      if (virtWinStart < 0)
  	virtWinStart = 0;
      }
  else
      {
      virtWinEnd += dinkAmount;
      if (virtWinEnd > virtSeqBaseCount)
         virtWinEnd = virtSeqBaseCount;
      }
  }
  
  long dinkSize(char *var)
  /* Return size to dink. */
  {
  char *stringVal = cartOptionalString(cart, var);
  double x;
  int fullInsideX = trackOffsetX(); /* The global versions of these are not yet set */
  int fullInsideWidth = tl.picWidth-gfxBorder-fullInsideX;
  double guideBases = (double)guidelineSpacing * (double)(virtWinEnd - virtWinStart)
                      / ((double)fullInsideWidth);
  
  if (stringVal == NULL || !isdigit(stringVal[0]))
      {
      stringVal = "1";
      cartSetString(cart, var, stringVal);
      }
  x = atof(stringVal);
  long ret = round(x*guideBases);
  
  return (ret == 0) ? 1 : ret;
  }
  
  void handlePostscript()
  /* Deal with Postscript output. */
  {
  struct tempName psTn, ideoPsTn;
  char *pdfFile = NULL, *ideoPdfFile = NULL;
  ZeroVar(&ideoPsTn);
  trashDirFile(&psTn, "hgt", "hgt", ".eps");
  
  if(!trackImgOnly)
      {
      printMenuBar();
  
      printf("<div style=\"margin: 10px\">\n");
      printf("<H1>PDF Output</H1>\n");
      printf("PDF images can be printed with Acrobat Reader "
             "and edited by many drawing programs such as Adobe "
             "Illustrator or Inkscape.<BR>");
      }
  doTrackForm(psTn.forCgi, &ideoPsTn);
  
  pdfFile = convertEpsToPdf(psTn.forCgi);
  if (strlen(ideoPsTn.forCgi))
      ideoPdfFile = convertEpsToPdf(ideoPsTn.forCgi);
  if (pdfFile != NULL)
      {
      printf("<UL style=\"margin-top:5px;\">\n");
      printf("<LI>Download <A TARGET=_blank HREF=\"%s\">"
         "the current browser graphic in PDF</A>\n", pdfFile);
      if (ideoPdfFile != NULL)
          printf("<LI>Download <A TARGET=_blank HREF=\"%s\">"
                 "the current chromosome ideogram in PDF</A>\n", ideoPdfFile);
      printf("</UL>\n");
      freez(&pdfFile);
      freez(&ideoPdfFile);
      // postscript
      printf("EPS (Postscript) images are a variant of PDF and easier to import into some "
              "drawing programs.\n");
      printf("<UL style=\"margin-top: 5px;\">\n");
      printf("<LI>Download <A HREF=\"%s\">the current browser graphic in EPS</A>", psTn.forCgi);
      if (strlen(ideoPsTn.forCgi))
          printf("<LI>Download <A HREF=\"%s\">the current chromosome ideogram in EPS</A>", ideoPsTn.forCgi);
      printf("</UL>\n");
  
      // see redmine #1077
      printf("<div style=\"margin-top:15px\">Tips for producing quality images for publication:</div>\n");
      printf("<UL style=\"margin-top:0px\">\n");
      printf("<LI>Add assembly name and chromosome range to the image on the\n"
          "<A HREF=\"hgTrackUi?g=ruler\">configuration page of the base position track</A>.\n");
      printf("<LI>If using the UCSC Genes track, consider showing only one transcript per gene by turning off splice variants on the track configuration page.\n");
      printf("<LI>Increase the font size and remove the light blue vertical guidelines in the \n"
          "<A HREF=\"hgTracks?hgTracksConfigPage=configure\">image configuration menu</A>.");
      printf("<LI>In the image configuration menu, change the size of the image,\n"
              "to make it look more square.\n");
      printf("</UL>\n");
      printf("</div>\n");
  
  
      }
  else
      printf("<BR><BR>PDF format not available");
  
  printf("<a href='%s?%s=%s'><input type='button' VALUE='Return to Browser'></a>\n",
             hgTracksName(), cartSessionVarName(), cartSessionId(cart));
  }
  
  void setRulerMode()
  /* Set the rulerMode variable from cart. */
  {
  char *s = cartUsualString(cart, RULER_TRACK_NAME, "dense");
  if (sameWord(s, "full") || sameWord(s, "on"))
      rulerMode = tvFull;
  else if (sameWord(s, "dense"))
      rulerMode = tvDense;
  else
      rulerMode = tvHide;
  }
  
  void setLayoutGlobals()
  /* Figure out basic dimensions of display.  */
  {
  withIdeogram = cartUsualBoolean(cart, "ideogram", TRUE);
  withLeftLabels = cartUsualBoolean(cart, "leftLabels", TRUE);
  withCenterLabels = cartUsualBoolean(cart, "centerLabels", TRUE);
  withGuidelines = cartUsualBoolean(cart, "guidelines", TRUE);
  if (!cartUsualBoolean(cart, "hgt.imageV1", FALSE))
      {
      withNextItemArrows = cartUsualBoolean(cart, "nextItemArrows", FALSE);
      withNextExonArrows = cartUsualBoolean(cart, "nextExonArrows", TRUE);
      }
  withExonNumbers = cartUsualBoolean(cart, "exonNumbers", TRUE);
  emAltHighlight = cartUsualBoolean(cart, "emAltHighlight", FALSE);
  if (!hIsGsidServer())
      {
      revCmplDisp = cartUsualBooleanDb(cart, database, REV_CMPL_DISP, FALSE);
      }
  emPadding = cartUsualInt(cart, "emPadding", emPadding);
  gmPadding = cartUsualInt(cart, "gmPadding", gmPadding);
  withPriorityOverride = cartUsualBoolean(cart, configPriorityOverride, FALSE);
  fullInsideX = trackOffsetX();
  fullInsideWidth = tl.picWidth-gfxBorder-fullInsideX;
  }
  
  static boolean resolvePosition(char **pPosition)
  /* Position may be an already-resolved chr:start-end, or a search term.
   * If it is a search term:
   * 1 match ==> set globals chromName, winStart, winEnd, return TRUE.
   * 0 matches ==> switch back to lastPosition, hopefully get 1 match from that;
   * set globals chromName, winStart, winEnd, return TRUE.  If no lastPosition, try w/hDefaultPos().
   * multiple matches ==> Display a page with links to match positions, return FALSE. */
  {
  boolean resolved = TRUE;
  struct dyString *dyWarn = dyStringNew(0);
  hgp = hgFindSearch(cart, pPosition, &chromName, &winStart, &winEnd, hgTracksName(), dyWarn);
  if (isNotEmpty(dyWarn->string))
      warn("%s", dyWarn->string);
  if (hgp->singlePos)
      {
      createHgFindMatchHash();
      }
  else
      {
      char *menuStr = menuBar(cart, database);
      if (menuStr)
          puts(menuStr);
      hgPositionsHtml(database, hgp, hgTracksName(), cart);
      resolved = FALSE;
      }
  cartSetString(cart, "position", *pPosition);
  return resolved;
  }
  
  void parseVirtPosition(char *position)
  /* parse virtual position
   *  TODO this is just temporary */
  {
  if (!position)
      {
      errAbort("position NULL");
      }
  char *vPos = cloneString(position);
  stripChar(vPos, ',');
  char *colon = strchr(vPos, ':');
  if (!colon)
      errAbort("position has no colon");
  char *dash = strchr(vPos, '-');
  if (!dash)
      errAbort("position has no dash");
  *colon = 0;
  *dash = 0;
  virtWinStart = atol(colon+1) - 1;
  virtWinEnd = atol(dash+1);
  }
  
  void parseNonVirtPosition(char *position)
  /* parse non-virtual position */
  {
  if (!position)
      {
      errAbort("position NULL");
      }
  char *vPos = cloneString(position);
  stripChar(vPos, ',');
  char *colon = strchr(vPos, ':');
  if (!colon)
      errAbort("position has no colon");
  char *dash = strchr(vPos, '-');
  if (!dash)
      errAbort("position has no dash");
  *colon = 0;
  *dash = 0;
  chromName = cloneString(vPos);
  winStart = atol(colon+1) - 1;
  winEnd = atol(dash+1);
  }
  
  boolean findNearestVirtMatch(char *chrom, int start, int end, boolean findNearest, long *retVirtStart, long *retVirtEnd)
  /* find nearest match on virt chrom.
   * findNearest flag means of no direct hits found, take the closest miss. */
  {
  // search for one or more overlapping windows
  struct positionMatch *mList = virtChromSearchForPosition(chrom, start, end, findNearest);
  
  // sort positions by virtPos (will be sorted by chrom, start, end)
  matchSortOnVPos(&mList);
  
  // merge contiguous matches spanning multiple touching windows
  mList = matchMergeContiguousVPos(mList);
  
  // TODO search for the best match in pList
  // TODO this is crude, needs to fix, just finds the largest match:
  struct positionMatch *p, *best = NULL;
  long bigSpan = 0;
  for (p=mList; p; p=p->next)
      {
      long span = p->virtEnd - p->virtStart;
      if (span > bigSpan)
  	{
  	bigSpan = span;
  	best = p;
  	}
      }
  if (best) // TODO do something better
      {
      // return the new location
      *retVirtStart = best->virtStart;
      *retVirtEnd   = best->virtEnd;
      }
  else
      {
      return FALSE;
      }
  return TRUE;
  }
  
  void remapHighlightPos()
  // Remap non-virt highlight position if any to new virtMode chrom.
  {
  if (virtualSingleChrom())
      return;
  struct highlightVar *h = parseHighlightInfo();
  if (h && h->db && sameString(h->db, database))
      {
      long virtStart = 0, virtEnd = 0;
      if (findNearestVirtMatch(h->chrom, h->chromStart, h->chromEnd, FALSE, &virtStart, &virtEnd)) // try to find the nearest match
  	{
  	// save new highlight position to cart var
  	char cartVar[1024];
  	safef(cartVar, sizeof cartVar, "%s.%s:%ld-%ld#%s", h->db, "virt", virtStart, virtEnd, h->hexColor);
  	cartSetString(cart, "highlight", cartVar);
  	}
      else
  	{
  	// erase the highlight cartvar if it has no overlap with the new virt chrom
  	cartRemove(cart, "highlight");
  	}
      }
  }
  
  
  void tracksDisplay()
  /* Put up main tracks display. This routine handles zooming and
   * scrolling. */
  {
  char titleVar[256];
  char *oldPosition = cartUsualString(cart, "oldPosition", "");
  boolean findNearest = cartUsualBoolean(cart, "findNearest", FALSE);
  cartRemove(cart, "findNearest");
  
  boolean positionIsVirt = FALSE;
  position = getPositionFromCustomTracks();
  if (NULL == position)
      {
      position = cartGetPosition(cart, database, &lastDbPosCart);
      if (sameOk(cgiOptionalString("position"), "lastDbPos"))
  	{
          restoreSavedVirtPosition();
  	}
      if (startsWith("virt:", position))
  	{
  	position = stripCommas(position); // sometimes the position string arrives with commas in it.
  	positionIsVirt = TRUE;
  	}
      }
  
  
  if (sameString(position, ""))
      {
      hUserAbort("Please go back and enter a coordinate range or a search term in the \"search term\" field.<br>For example: chr22:20100000-20200000.\n");
      }
  
  if (!positionIsVirt)
      {
      if (! resolvePosition(&position))
          return;
      }
  
  virtMode = cartUsualBoolean(cart, "virtMode", FALSE);
  
  /* Figure out basic dimensions of display.  This
   * needs to be done early for the sake of the
   * zooming and dinking routines. */
  setLayoutGlobals();
  
  virtModeType = cartUsualString(cart, "virtModeType", virtModeType);
  
  if (positionIsVirt && virtualSingleChrom())
      {
      // we need chromName to be set before initRegionList() gets called.
      position = cartUsualString(cart, "nonVirtPosition", "");
      if (!sameString(position,""))
  	parseNonVirtPosition(position);
      }
  
  // TODO GALT do we need to add in other types that now depend on emGeneTable too? maybe singleTrans?
  //   OR maybe this code should just be part of initRegionList()
  if (sameString(virtModeType, "exonMostly") || sameString(virtModeType, "geneMostly"))
      {
      setEMGeneTrack();
      if (!emGeneTable) // there is no available gene table, undo exonMostly or geneMostly
  	{
  	//warn("setEMGeneTrack unable to find default gene track");
  	virtModeType = "default";
  	cartSetString(cart, "virtModeType", virtModeType);
  	}
      }
  
  lastVirtModeType = cartUsualString(cart, "lastVirtModeType", lastVirtModeType);
  
  while(TRUE)
      {
      if (sameString(virtModeType, "default") && !(sameString(lastVirtModeType, "default")))
  	{ // RETURNING TO DEFAULT virtModeType
  	virtModeType = "default";
  	cartSetString(cart, "virtModeType", virtModeType);
  	findNearest = TRUE;
  	if (positionIsVirt)
  	    position = cartUsualString(cart, "nonVirtPosition", "");
  	char *nvh = cartUsualString(cart, "nonVirtHighlight", NULL);
  	if (nvh)
  	    cartSetString(cart, "highlight", nvh);
  	if (!sameString(position,""))
  	    parseNonVirtPosition(position);
  	}
  
      if (initRegionList())   // initialize the region list, sets virtModeExtraState
  	{
  	break;
  	}
      else
  	{ // virt mode failed, forced to return to default
  	virtModeType = "default";
  	cartSetString(cart, "virtModeType", virtModeType);
  	position = cloneString(hDefaultPos(database));
          resolvePosition(&position);
  	positionIsVirt=FALSE;
  	virtMode=FALSE;
  	}
      }
  
  // PAD padding of exon regions is now being done inside the fetch/merge.
  //if (emPadding > 0)
      //padVirtRegions(emPadding); // this old routine does not handle multiple chroms yet
  
  //testRegionList(); // check if it is ascending non-overlapping regions. (this is not the case with custom user-defined-regions)
  
  makeVirtChrom();
  
  //testVirtChromBinarySearch();
  
  // ajax callback to convert chrom position to virt chrom position
  if (cartVarExists(cart, "hgt.convertChromToVirtChrom"))
      {
      position = cartString(cart, "hgt.convertChromToVirtChrom");
      char nvh[256];
      safef(nvh, sizeof nvh, "%s.%s", database, position);
      cartSetString(cart, "nonVirtHighlight", nvh);
      parseNonVirtPosition(position);
      if (findNearestVirtMatch(chromName, winStart, winEnd, FALSE, &virtWinStart, &virtWinEnd))
  	{
  	struct jsonElement *jsonForConvert = NULL;
  	jsonForConvert = newJsonObject(newHash(8));
  	jsonObjectAdd(jsonForConvert, "virtWinStart", newJsonNumber(virtWinStart));
  	jsonObjectAdd(jsonForConvert, "virtWinEnd", newJsonNumber(virtWinEnd));
  
  	struct dyString *dy = dyStringNew(1024);
  	jsonDyStringPrint(dy, (struct jsonElement *) jsonForConvert, "convertChromToVirtChrom", 0);
  	jsInline(dy->string);
  	dyStringFree(&dy);
  	}
      return;
      }
  
  lastVirtModeExtraState = cartUsualString(cart, "lastVirtModeExtraState", lastVirtModeExtraState);
  
  // DISGUISED POSITION
  if (!startsWith("virt:", position) && (virtualSingleChrom()))
      {
      // "virtualSingleChrom trying to find best vchrom location corresponding to chromName, winStart, winEnd
      findNearest = TRUE;
  
       // try to find the nearest match
      if (!(chromName && findNearestVirtMatch(chromName, winStart, winEnd, findNearest, &virtWinStart, &virtWinEnd)))
  	{ // create 10k window near middle of vchrom
  	warn("Your new regions are not near previous location. Using middle of new coordinates.");
  	virtWinStart = virtSeqBaseCount / 2;
  	virtWinEnd = virtWinStart + 10000;
  	if (virtWinEnd > virtSeqBaseCount)
  	    virtWinEnd = virtSeqBaseCount;
  	}
      virtMode = TRUE;
      }
  
  // when changing modes (or state like padding), first try to revert to plain non-virt position
  if (!sameString(virtModeType, "default")
   && !sameString(lastVirtModeType, "default")
   && !(sameString(virtModeType, lastVirtModeType) && sameString(virtModeExtraState, lastVirtModeExtraState)))
      { // CHANGE FROM ONE NON-DEFAULT virtMode to another.
      virtChromChanged = TRUE;    // virtChrom changed
      lastVirtModeType = "default";
      cartSetString(cart, "lastVirtModeType", lastVirtModeType); // I think I do not need this
      lastVirtModeExtraState = "";
      findNearest = TRUE;
      position = cartUsualString(cart, "nonVirtPosition", "");
      if (!sameString(position,""))
  	parseNonVirtPosition(position);
      char *nvh = cartUsualString(cart, "nonVirtHighlight", "");
      if (!sameString(nvh, "")) // REMOVE? not needed probably
  	{
  	cartSetString(cart, "highlight", nvh);
  	}
      }
  
  // virt mode has not changed
  if (sameString(virtModeType, lastVirtModeType)
   && sameString(virtModeExtraState, lastVirtModeExtraState))
      {
      if (virtMode)
  	{
  	if (positionIsVirt)
  	    {
  	    parseVirtPosition(position);
  	    }
  	else
  	    {	
  	    // Is this a new position to navigate to
  	    // or just an old inherited position.
  	    position = stripCommas(position);  // sometimes the position string arrives with commas in it.
  	    if (!sameString(position, oldPosition))
  		{
  		if (!findNearestVirtMatch(chromName, winStart, winEnd, findNearest, &virtWinStart, &virtWinEnd))
  		    {
  		    // errAbort has kind of harsh behavior, and does not work well with ajax anyways
  		    warn("Location not found in Multi-Region View. "
  		    "To return to default view at that location, "
  		    "click <a href=%s?%s=%s&position=%s:%d-%d&virtModeType=default>here</a>.\n"
  		    , hgTracksName(), cartSessionVarName(), cartSessionId(cart), chromName, winStart+1, winEnd);
  		    // try to resume using oldPosition
  		    parseVirtPosition(oldPosition);
  		    }
  		}
  	    }
  	}
      else
  	{
  	if (positionIsVirt)
  	    errAbort("positionIsVirt=%d but virtMode=%d", positionIsVirt, virtMode);
  	}
  
  
      }
  else
      {
  
      if (sameString(virtModeType,"default"))  // we are leaving virtMode
  	{
  	virtMode = FALSE;
          cartRemove(cart, "virtWinFull");
          cartRemove(cart, "virtShortDesc");
  	}
      else
  	{
  
  	// ENTERING VIRTMODE
  
  	// First time initialization
  
  	findNearest = TRUE;
  
  	// For now, do this manually here:
  	// sets window to full genome size, which for these demos should be small except for allChroms
  	if (sameString(virtModeType, "exonMostly") || 
              sameString(virtModeType, "geneMostly") || 
              sameString(virtModeType, "kcGenes") ||
              (sameString(virtModeType, "customUrl") && 
                      !cartUsualBoolean(cart, "virtWinFull", FALSE)))
  	    {
  	    // trying to find best vchrom location corresponding to chromName, winStart, winEnd);
  	    // try to find the nearest match
  	    if (!(chromName && findNearestVirtMatch(chromName, winStart, winEnd, findNearest, &virtWinStart, &virtWinEnd)))
  		{ // create 10k window near middle of vchrom
  		warn("Your new regions are not near previous location. Using middle of new coordinates.");
  		virtWinStart = virtSeqBaseCount / 2;
  		virtWinEnd = virtWinStart + 10000;
  		if (virtWinEnd > virtSeqBaseCount)
  		    virtWinEnd = virtSeqBaseCount;
  		}
  	    virtMode = TRUE;
  	    }
  	else if (sameString(virtModeType, "singleAltHaplo"))
  	    {
  	    virtWinStart = defaultVirtWinStart;
  	    virtWinEnd = defaultVirtWinEnd;
  	    virtMode = TRUE;
  	    }
  	else if (!sameString(virtModeType, "default"))
  	    { // try to set view to entire vchrom
  	    virtWinStart = 0;
  	    virtWinEnd = virtSeqBaseCount;
  	    virtMode = TRUE;
  	    // TODO what if the full-vchrom view has "too many windows"
  	    // check if virtRegionCount > 4000?
  	    }
  
  	remapHighlightPos();
  
  	}
  
      }
  
  if (virtMode)
      virtChromName = "virt";
  else
      virtChromName = chromName;
  
  virtWinBaseCount = virtWinEnd - virtWinStart;
  
  
  baseShowPos = cartUsualBoolean(cart, BASE_SHOWPOS, FALSE);
  baseShowAsm = cartUsualBoolean(cart, BASE_SHOWASM, FALSE);
  baseShowScaleBar = cartUsualBoolean(cart, BASE_SCALE_BAR, TRUE);
  baseShowRuler = cartUsualBoolean(cart, BASE_SHOWRULER, TRUE);
  safef(titleVar,sizeof(titleVar),"%s_%s", BASE_TITLE, database);
  baseTitle = cartUsualString(cart, titleVar, "");
  if (sameString(baseTitle, ""))
      baseTitle = NULL;
  
  if  (cgiVarExists("hgt.toggleRevCmplDisp"))
      toggleRevCmplDisp();
  setRulerMode();
  
  /* Do zoom/scroll if they hit it. */
  if (cgiVarExists("hgt.left3"))
      relativeScroll(-0.95);
  else if (cgiVarExists("hgt.left2"))
      relativeScroll(-0.475);
  else if (cgiVarExists("hgt.left1"))
      relativeScroll(-0.1);
  else if (cgiVarExists("hgt.right1"))
      relativeScroll(0.1);
  else if (cgiVarExists("hgt.right2"))
      relativeScroll(0.475);
  else if (cgiVarExists("hgt.right3"))
      relativeScroll(0.95);
  else if (cgiVarExists("hgt.inBase"))
      zoomToBaseLevel();
  else if (cgiVarExists("hgt.in3"))
      zoomAroundCenter(1.0/10.0);
  else if (cgiVarExists("hgt.in2"))
      zoomAroundCenter(1.0/3.0);
  else if (cgiVarExists("hgt.in1"))
      zoomAroundCenter(1.0/1.5);
  else if (cgiVarExists("hgt.out1"))
      zoomAroundCenter(1.5);
  else if (cgiVarExists("hgt.out2"))
      zoomAroundCenter(3.0);
  else if (cgiVarExists("hgt.out3"))
      zoomAroundCenter(10.0);
  else if (cgiVarExists("hgt.out4"))
      zoomAroundCenter(100.0);
  else if (cgiVarExists("hgt.dinkLL"))
      dinkWindow(TRUE, -dinkSize("dinkL"));
  else if (cgiVarExists("hgt.dinkLR"))
      dinkWindow(TRUE, dinkSize("dinkL"));
  else if (cgiVarExists("hgt.dinkRL"))
      dinkWindow(FALSE, -dinkSize("dinkR"));
  else if (cgiVarExists("hgt.dinkRR"))
      dinkWindow(FALSE, dinkSize("dinkR"));
  
  /* Before loading items, deal with the next/prev item arrow buttons if pressed. */
  if (cgiVarExists("hgt.nextItem"))
      doNextPrevItem(TRUE, cgiUsualString("hgt.nextItem", NULL));
  else if (cgiVarExists("hgt.prevItem"))
      doNextPrevItem(FALSE, cgiUsualString("hgt.prevItem", NULL));
  
  
  /* Clip chromosomal position to fit. */
  if (virtWinEnd < virtWinStart)
      {
      // swap start and end (user entered coordinates backwards)
      long temp = virtWinEnd;
      virtWinEnd = virtWinStart;
      virtWinStart = temp;
      }
  else if (virtWinStart == virtWinEnd)
      {
      // Size 0 window
      virtWinStart -= 1;
      virtWinEnd += 1;
      }
  
  if (virtWinStart < 0)
      {
      virtWinStart = 0;
      }
  
  if (virtWinEnd > virtSeqBaseCount)
      {
      virtWinEnd = virtSeqBaseCount;
      }
  
  if (virtWinStart > virtSeqBaseCount)
      {
      virtWinStart = virtSeqBaseCount - 1000;
      }
  
  virtWinBaseCount = virtWinEnd - virtWinStart;
  if (virtWinBaseCount <= 0)
      hUserAbort("Window out of range on %s", virtChromName);
  
  if (!cartUsualBoolean(cart, "hgt.psOutput", FALSE)
   && !cartUsualBoolean(cart, "hgt.imageV1" , FALSE))
      {
  
      // TODO GALT Guidelines broken on virtChrom for 3X.
      //  works in demo0 or real chrom. Only the guidelines seem to be messed up.
      //  Other stuff works. 1X works too.
      // Since we are not using 3X for now, I will leave this for a future fix.
      // To test 3X, do make clean; make CFLAGS=-DIMAGEv2_DRAG_SCROLL_SZ=3
  
      // Start an imagebox (global for now to avoid huge rewrite of hgTracks)
      // Set up imgBox dimensions
      int sideSliceWidth  = 0;   // Just being explicit
      if (withLeftLabels)
          sideSliceWidth   = (fullInsideX - gfxBorder*3) + 2;
  
      //  for the 3X expansion effect to work, this needs to happen BEFORE we create the windows list
      //    in makeWindowListFromVirtChrom()
      theImgBox = imgBoxStart(database,virtChromName,virtWinStart,virtWinEnd,
                              (!revCmplDisp),sideSliceWidth,tl.picWidth);
      // Define a portal with a default expansion size,
      // then set the global dimensions to the full image size
      if (imgBoxPortalDefine(theImgBox,&virtWinStart,&virtWinEnd,&(tl.picWidth),0))
          {
          virtWinBaseCount = virtWinEnd - virtWinStart;
          fullInsideWidth = tl.picWidth - gfxBorder - fullInsideX;
          }
  
      }
  
  
  // For portal 3x expansion to work right, it would have to take effect, at least temporarily,
  // right here before we call makeWindowListFromVirtChrom().
  windows = makeWindowListFromVirtChrom(virtWinStart, virtWinEnd); // creates windows, sets chrom, winStart, winEnd from virtual chrom
  if (slCount(windows) > 4000) // TODO a more graceful response
  	errAbort("Too many windows in view. Unable to display image at requested zoom level.");
  
  allocPixelsToWindows(); // sets windows insideWidth and insideX
  
  if (theImgBox)
      {
      // If a portal was established, then set the global dimensions back to the portal size
      if (imgBoxPortalDimensions(theImgBox,NULL,NULL,NULL,NULL,&virtWinStart,&virtWinEnd,&(tl.picWidth),NULL))
          {
          virtWinBaseCount = virtWinEnd - virtWinStart;
          fullInsideWidth = tl.picWidth-gfxBorder-fullInsideX;
          }
      }
  
  setGlobalsFromWindow(windows); // first window
  
  seqBaseCount = hChromSize(database, chromName);
  
  /* Save computed position in cart. */
  cartSetString(cart, "org", organism);
  cartSetString(cart, "db", database);
  
  char newPos[256];
  
  // disguise the cart pos var
  if (virtualSingleChrom()) // DISGUISE VMODE
      safef(newPos, sizeof newPos, "%s", windowsSpanPosition());
  else // usual
      safef(newPos, sizeof newPos, "%s:%ld-%ld", virtChromName, virtWinStart+1, virtWinEnd);
  
  position = cloneString(newPos);
  cartSetString(cart, "position", position);
  cartSetString(cart, "oldPosition", position);
  //cartSetString(cart, "lastPosition", position);  // this is set in cart.c
  
  cartSetBoolean(cart, "virtMode", virtMode);
  cartSetString(cart, "virtModeType", virtModeType);
  virtModeType = cartString(cart, "virtModeType"); // refresh the pointer after changing hash
  
  
  lastVirtModeType=virtModeType;
  cartSetString(cart, "lastVirtModeType", lastVirtModeType);
  lastVirtModeType = cartString(cart, "lastVirtModeType"); // refresh
  
  lastVirtModeExtraState=virtModeExtraState;
  cartSetString(cart, "lastVirtModeExtraState", lastVirtModeExtraState);
  lastVirtModeExtraState = cartString(cart, "lastVirtModeExtraState"); // refresh
  
  
  // save a quick position to use if user leaves virtMode.
  if (virtMode)
      cartSetString(cart, "nonVirtPosition", nonVirtPositionFromWindows());
  else
      cartRemove(cart, "nonVirtPosition");
  
  // save a highlight position to use if user leaves virtMode.
  char *nvh = NULL;
  if (virtMode)
     nvh = nonVirtPositionFromHighlightPos();
  if (virtMode && nvh)
      cartSetString(cart, "nonVirtHighlight", nvh);
  else
      cartRemove(cart, "nonVirtHighlight");
  
  // save lastDbPos. save the current position and other important cart vars related to virtual view.
  
  lastDbPosSaveCartSetting("position");
  lastDbPosSaveCartSetting("nonVirtPosition");
  lastDbPosSaveCartSetting("virtMode");
  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);
  
  }
  
  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();
  }
  
  void chromInfoRowsChromExt(char *sortType)
  /* Make table rows of chromosomal chromInfo name & size, sorted by name. */
  {
  struct slName *chromList = hAllChromNames(database);
  struct slName *chromPtr = NULL;
  long long total = 0;
  
  if (sameString(sortType,"default"))
      slSort(&chromList, chrSlNameCmp);
  else if (sameString(sortType,"withAltRandom"))
      slSort(&chromList, chrSlNameCmpWithAltRandom);
  else
      errAbort("unknown sort type in chromInfoRowsChromExt: %s", sortType);
  
  for (chromPtr = chromList;  chromPtr != NULL;  chromPtr = chromPtr->next)
      {
      unsigned size = hChromSize(database, chromPtr->name);
      cgiSimpleTableRowStart();
      cgiSimpleTableFieldStart();
      htmlPrintf("<A HREF=\"%s|none|?%s|url|=%s|url|&position=%s|url|\">%s</A>",
             hgTracksName(), cartSessionVarName(), cartSessionId(cart),
             chromPtr->name, chromPtr->name);
      cgiTableFieldEnd();
      cgiTableFieldStartAlignRight();
      printLongWithCommas(stdout, size);
      puts("&nbsp;&nbsp;");
      cgiTableFieldEnd();
      cgiTableRowEnd();
      total += size;
      }
  chromInfoTotalRow(slCount(chromList), total);
  slFreeList(&chromList);
  }
  
  void chromInfoRowsChrom()
  /* Make table rows of chromosomal chromInfo name & size, sorted by name. */
  {
  chromInfoRowsChromExt("default");
  }
  
  static int  chromInfoCmpSize(const void *va, const void *vb)
  /* Compare to sort based on chrom size */
  {
  const struct chromInfo *a = *((struct chromInfo **)va);
  const struct chromInfo *b = *((struct chromInfo **)vb);
  
  return b->size - a->size;
  }
  
  void chromInfoRowsNonChromTrackHub(int limit)
  /* Make table rows of non-chromosomal chromInfo name & size */
  /* leaks chromInfo list */
  {
  struct chromInfo *chromInfo = trackHubAllChromInfo(database);
  slSort(&chromInfo, chromInfoCmpSize);
  int seqCount = slCount(chromInfo);
  long long total = 0;
  char msg1[512], msg2[512];
  boolean truncating;
  int count = limit;
  
  truncating = (limit > 0) && (seqCount > limit);
  
  for(;count-- && (chromInfo != NULL); chromInfo = chromInfo->next)
      {
      unsigned size = chromInfo->size;
      cgiSimpleTableRowStart();
      cgiSimpleTableFieldStart();
      htmlPrintf("<A HREF=\"%s|none|?%s|url|=%s|url|&position=%s|url|\">%s</A>",
             hgTracksName(), cartSessionVarName(), cartSessionId(cart),
             chromInfo->chrom,chromInfo->chrom);
      cgiTableFieldEnd();
      cgiTableFieldStartAlignRight();
      printLongWithCommas(stdout, size);
      puts("&nbsp;&nbsp;");
      cgiTableFieldEnd();
      cgiTableRowEnd();
      total += size;
      }
  if (!truncating)
      {
      chromInfoTotalRow(seqCount, total);
      }
  else
      {
      safef(msg1, sizeof(msg1), "Limit reached");
      safef(msg2, sizeof(msg2), "%d rows displayed", limit);
      cgiSimpleTableRowStart();
      cgiSimpleTableFieldStart();
      puts(msg1);
      cgiTableFieldEnd();
      cgiSimpleTableFieldStart();
      puts(msg2);
      cgiTableFieldEnd();
      for(;limit-- && (chromInfo != NULL); chromInfo = chromInfo->next)
  	total += chromInfo->size;
  
      unsigned scafCount = seqCount;
      cgiTableRowEnd();
      safef(msg1, sizeof(msg1), "contig/scaffold<BR>count:");
      safef(msg2, sizeof(msg2), "total size:");
      cgiSimpleTableRowStart();
      cgiSimpleTableFieldStart();
      puts(msg1);
      cgiTableFieldEnd();
      cgiSimpleTableFieldStart();
      puts(msg2);
      cgiTableFieldEnd();
      cgiTableRowEnd();
      cgiSimpleTableRowStart();
      cgiSimpleTableFieldStart();
      printLongWithCommas(stdout, scafCount);
      cgiTableFieldEnd();
      cgiSimpleTableFieldStart();
      printLongWithCommas(stdout, total);
      cgiTableFieldEnd();
      cgiTableRowEnd();
      }
  }
  
  void chromInfoRowsNonChrom(int limit)
  /* Make table rows of non-chromosomal chromInfo name & size, sorted by size. */
  {
  if (trackHubDatabase(database))
      {
      chromInfoRowsNonChromTrackHub(limit);
      return;
      }
  
  struct sqlConnection *conn = hAllocConn(database);
  struct sqlResult *sr = NULL;
  char **row = NULL;
  long long total = 0;
  char query[512];
  char msg1[512], msg2[512];
  int seqCount = 0;
  boolean truncating;
  
  seqCount = sqlQuickNum(conn, NOSQLINJ "select count(*) from chromInfo");
  truncating = (limit > 0) && (seqCount > limit);
  
  if (!truncating)
      {
      sr = sqlGetResult(conn, NOSQLINJ "select chrom,size from chromInfo order by size desc");
      }
  else
      {
  
      sqlSafef(query, sizeof(query), "select chrom,size from chromInfo order by size desc limit %d", limit);
      sr = sqlGetResult(conn, query);
      }
  
  while ((row = sqlNextRow(sr)) != NULL)
      {
      unsigned size = sqlUnsigned(row[1]);
      cgiSimpleTableRowStart();
      cgiSimpleTableFieldStart();
      htmlPrintf("<A HREF=\"%s|none|?%s|url|=%s|url|&position=%s|url|\">%s</A>",
             hgTracksName(), cartSessionVarName(), cartSessionId(cart),
             row[0], row[0]);
      cgiTableFieldEnd();
      cgiTableFieldStartAlignRight();
      printLongWithCommas(stdout, size);
      puts("&nbsp;&nbsp;");
      cgiTableFieldEnd();
      cgiTableRowEnd();
      total += size;
      }
  if (!truncating)
      {
      chromInfoTotalRow(seqCount, total);
      }
  else
      {
      safef(msg1, sizeof(msg1), "Limit reached");
      safef(msg2, sizeof(msg2), "%d rows displayed", limit);
      cgiSimpleTableRowStart();
      cgiSimpleTableFieldStart();
      puts(msg1);
      cgiTableFieldEnd();
      cgiSimpleTableFieldStart();
      puts(msg2);
      cgiTableFieldEnd();
      sqlFreeResult(&sr);
      sqlSafef(query, sizeof(query), "select count(*),sum(size) from chromInfo");
      sr = sqlGetResult(conn, query);
      if ((row = sqlNextRow(sr)) != NULL)
  	{
  	unsigned scafCount = sqlUnsigned(row[0]);
  	long long totalSize = sqlLongLong(row[1]);
  	cgiTableRowEnd();
  	safef(msg1, sizeof(msg1), "contig/scaffold<BR>count:");
  	safef(msg2, sizeof(msg2), "total size:");
  	cgiSimpleTableRowStart();
  	cgiSimpleTableFieldStart();
  	puts(msg1);
  	cgiTableFieldEnd();
  	cgiSimpleTableFieldStart();
  	puts(msg2);
  	cgiTableFieldEnd();
  	cgiTableRowEnd();
  	cgiSimpleTableRowStart();
  	cgiSimpleTableFieldStart();
  	printLongWithCommas(stdout, scafCount);
  	cgiTableFieldEnd();
  	cgiSimpleTableFieldStart();
  	printLongWithCommas(stdout, totalSize);
  	cgiTableFieldEnd();
  	}
      cgiTableRowEnd();
      }
  sqlFreeResult(&sr);
  hFreeConn(&conn);
  }
  
  static void chromSizesDownloadRow()
  /* Show link to chrom.sizes file at end of chromInfo table (unless this is a hub) */
  {
  if (! trackHubDatabase(database))
      {
      cgiSimpleTableRowStart();
      cgiSimpleTableFieldStart();
      puts("Download as file");
      cgiTableFieldEnd();
      cgiSimpleTableFieldStart();
      printf("<A HREF='http://%s/goldenPath/%s/bigZips/%s.chrom.sizes'>%s.chrom.sizes</A>",
             hDownloadsServer(), database, database, database);
      cgiTableFieldEnd();
      cgiTableRowEnd();
      }
  }
  
  void chromInfoPage()
  /* Show list of chromosomes (or scaffolds, etc) on which this db is based. */
  {
  char *position = cartUsualString(cart, "position", hDefaultPos(database));
  char *defaultChrom = hDefaultChrom(database);
  char *freeze = hFreezeFromDb(database);
  struct dyString *title = dyStringNew(512);
  if (freeze == NULL)
      dyStringPrintf(title, "%s Browser Sequences",
  		   hOrganism(database));
  else if (stringIn(database, freeze))
      dyStringPrintf(title, "%s %s Browser Sequences",
  		   hOrganism(database), freeze);
  else
      dyStringPrintf(title, "%s %s (%s) Browser Sequences",
  		   trackHubSkipHubName(hOrganism(database)), freeze, trackHubSkipHubName(database));
  webStartWrapperDetailedNoArgs(cart, database, "", title->string, FALSE, FALSE, FALSE, FALSE);
  printf("<FORM ACTION=\"%s\" NAME=\"posForm\" METHOD=GET>\n", hgTracksName());
  cartSaveSession(cart);
  
  puts("Enter a position, or click on a sequence name to view the entire "
       "sequence in the genome browser.<P>");
  puts("position ");
  hTextVar("position", addCommasToPos(database, position), 30);
  cgiMakeButton("Submit", "submit");
  puts("<P>");
  
  hTableStart();
  cgiSimpleTableRowStart();
  cgiSimpleTableFieldStart();
  puts("Sequence name &nbsp;");
  cgiTableFieldEnd();
  cgiSimpleTableFieldStart();
  puts("Length (bp) including gaps &nbsp;");
  cgiTableFieldEnd();
  cgiTableRowEnd();
  
  if (sameString(database,"hg38"))
      chromInfoRowsChromExt("withAltRandom");
  else if ((startsWith("chr", defaultChrom) || startsWith("Group", defaultChrom)) &&
      hChromCount(database) < 100)
      chromInfoRowsChrom();
  else
      chromInfoRowsNonChrom(1000);
  chromSizesDownloadRow();
  
  hTableEnd();
  cgiDown(0.9);
  
  hgPositionsHelpHtml(organism, database);
  puts("</FORM>");
  dyStringFree(&title);
  webEndSectionTables();
  }
  
  
  void resetVars()
  /* Reset vars except for position and database. */
  {
  static char *except[] = {"db", "position", NULL};
  char *cookieName = hUserCookie();
  char *sessionId = cgiOptionalString(cartSessionVarName());
  char *userId = findCookieData(cookieName);
  struct cart *oldCart = cartNew(userId, sessionId, NULL, NULL);
  cartRemoveExcept(oldCart, except);
  cartCheckout(&oldCart);
  cgiVarExcludeExcept(except);
  }
  
  void setupHotkeys(boolean gotExtTools)
  /* setup keyboard shortcuts and a help dialog for it */
  {
  struct dyString *dy = dyStringNew(1024);
  // wire the keyboard hotkeys
  
  // left
  dyStringPrintf(dy,"Mousetrap.bind('ctrl+j', function() { $('input[name=\"hgt.left1\"]').click(); return false; }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('j', function() { $('input[name=\"hgt.left2\"]').click() }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('J', function() { $('input[name=\"hgt.left3\"]').click() }); \n");
  
  // right
  dyStringPrintf(dy,"Mousetrap.bind('ctrl+l', function() { $('input[name=\"hgt.right1\"]').click(); return false; }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('l', function() { $('input[name=\"hgt.right2\"]').click() }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('L', function() { $('input[name=\"hgt.right3\"]').click() }); \n");
  
  // zoom in
  dyStringPrintf(dy,"Mousetrap.bind('ctrl+i', function() { $('input[name=\"hgt.in1\"]').click(); return false; }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('i', function() { $('input[name=\"hgt.in2\"]').click() }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('I', function() { $('input[name=\"hgt.in3\"]').click() }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('b', function() { $('input[name=\"hgt.inBase\"]').click() }); \n");
  
  // zoom out
  dyStringPrintf(dy,"Mousetrap.bind('ctrl+k', function() { $('input[name=\"hgt.out1\"]').click(); return false; }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('k', function() { $('input[name=\"hgt.out2\"]').click() }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('K', function() { $('input[name=\"hgt.out3\"]').click() }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('0', function() { $('input[name=\"hgt.out4\"]').click() }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('ctrl+k', function() { $('input[name=\"hgt.out1\"]').click(); return false; }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('k', function() { $('input[name=\"hgt.out2\"]').click() }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('K', function() { $('input[name=\"hgt.out3\"]').click() }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('0', function() { $('input[name=\"hgt.out4\"]').click() }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('1', function() { zoomTo(50);} ); \n");
  dyStringPrintf(dy,"Mousetrap.bind('2', function() { zoomTo(500);} ); \n");
  dyStringPrintf(dy,"Mousetrap.bind('3', function() { zoomTo(5000);} ); \n");
  dyStringPrintf(dy,"Mousetrap.bind('4', function() { zoomTo(50000);} ); \n");
  dyStringPrintf(dy,"Mousetrap.bind('5', function() { zoomTo(500000);} ); \n");
  dyStringPrintf(dy,"Mousetrap.bind('6', function() { zoomTo(5000000);} ); \n");
  
  // buttons
  dyStringPrintf(dy,"Mousetrap.bind('c f', function() { $('input[name=\"hgTracksConfigPage\"]').click() }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('t s', function() { $('input[name=\"hgt_tSearch\"]').click() }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('h a', function() { $('input[name=\"hgt.hideAll\"]').click() }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('d t', function() { $('input[name=\"hgt.reset\"]').click() }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('d o', function() { $('input[name=\"hgt.defaultImgOrder\"]').click() }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('c t', function() { document.customTrackForm.submit();return false; }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('t h', function() { document.trackHubForm.submit();return false; }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('t c', function() { document.editHubForm.submit();return false; }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('r s', function() { $('input[name=\"hgt.setWidth\"]').click() }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('r f', function() { $('input[name=\"hgt.refresh\"]').click() }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('r v', function() { $('input[name=\"hgt.toggleRevCmplDisp\"]').click() }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('v d', gotoGetDnaPage); \n");
  
  // highlight
  dyStringPrintf(dy,"Mousetrap.bind('h c', function() { highlightCurrentPosition('clear'); }); \n");
  dyStringPrintf(dy,"Mousetrap.bind('h m', function() { highlightCurrentPosition('add'); }); \n");
  //dyStringPrintf(dy,"Mousetrap.bind('h n', function() { highlightCurrentPosition('new'); }); \n"); superfluos as it is just hc + hm?
  
  // focus
  dyStringPrintf(dy,"Mousetrap.bind('/', function() { $('input[name=\"hgt.positionInput\"]').focus(); return false; }, 'keydown'); \n");
  dyStringPrintf(dy,"Mousetrap.bind('?', showHotkeyHelp);\n");
  
  // menu
  if (gotExtTools)
      dyStringPrintf(dy,"Mousetrap.bind('s t', showExtToolDialog); \n");
  
  // multi-region views
  dyStringPrintf(dy,"Mousetrap.bind('e v', function() { window.location.href='%s?%s=%s&virtModeType=exonMostly'; });  \n",
             hgTracksName(), cartSessionVarName(), cartSessionId(cart));
  dyStringPrintf(dy,"Mousetrap.bind('d v', function() { window.location.href='%s?%s=%s&virtModeType=default'; });  \n",
             hgTracksName(), cartSessionVarName(), cartSessionId(cart));
  
  dyStringPrintf(dy,"Mousetrap.bind('v s', function() { window.location.href='%s?chromInfoPage=&%s=%s'; });  \n",
             hgTracksName(), cartSessionVarName(), cartSessionId(cart));
  
  // links to a few tools
  dyStringPrintf(dy,"Mousetrap.bind('t b', function() { $('#blatMenuLink')[0].click()});\n");
  dyStringPrintf(dy,"Mousetrap.bind('t i', function() { $('#ispMenuLink')[0].click()});\n");
  dyStringPrintf(dy,"Mousetrap.bind('t t', function() { $('#tableBrowserMenuLink')[0].click()});\n");
  dyStringPrintf(dy,"Mousetrap.bind('c r', function() { $('#cartResetMenuLink')[0].click()});\n");
  dyStringPrintf(dy,"Mousetrap.bind('s s', function() { $('#sessionsMenuLink')[0].click()});\n");
  dyStringPrintf(dy,"Mousetrap.bind('p s', function() { $('#publicSessionsMenuLink')[0].click()});\n");
  
  // also add an entry to the help menu that shows the keyboard shortcut help dialog
  dyStringPrintf(dy,"$(document).ready(addKeyboardHelpEntries);\n");
  
  jsInline(dy->string);
  dyStringFree(&dy);
  
  // help dialog
  hPrintf("<div style=\"display:none\" id=\"hotkeyHelp\" title=\"Keyboard shortcuts\">\n");
  hPrintf("<table style=\"width:600px; border-color:#666666; border-collapse:collapse\">\n");
  hPrintf("<tr><td style=\"width:18ch\">left 10&#37;</td><td width=\"auto\" class=\"hotkey\">ctrl+j</td>  <td style=\"width:24ch\"> track search</td><td class=\"hotkey\">t then s</td>               </tr>\n"); // percent sign
  hPrintf("<tr><td> left 1/2 screen</td><td class=\"hotkey\">j</td>   <td> default tracks</td><td class=\"hotkey\">d then t</td>             </tr>\n");
  hPrintf("<tr><td> left one screen</td><td class=\"hotkey\">J</td>   <td> default order</td><td class=\"hotkey\">d then o</td>              </tr>\n");
  hPrintf("<tr><td> right 10&#37;</td><td class=\"hotkey\">ctrl+l</td><td> hide all</td><td class=\"hotkey\">h then a</td>                   </tr>\n"); // percent sign
  hPrintf("<tr><td> right 1/2 screen</td><td class=\"hotkey\">l</td>  <td> custom tracks</td><td class=\"hotkey\">c then t</td>              </tr>\n");
  hPrintf("<tr><td> right one screen</td><td class=\"hotkey\">L</td>  <td> track collections</td><td class=\"hotkey\">t then c</td>                 </tr>\n");
- hPrintf("<tr><td> right one screen</td><td class=\"hotkey\">L</td>  <td> track hubs</td><td class=\"hotkey\">t then h</td>                 </tr>\n");
+ hPrintf("<tr><td> jump to position box</td><td class=\"hotkey\">/</td>  <td> track hubs</td><td class=\"hotkey\">t then h</td>                 </tr>\n");
  hPrintf("<tr><td> zoom in 1.5x</td><td class=\"hotkey\">ctrl+i</td> <td> configure</td><td class=\"hotkey\">c then f</td>                  </tr>\n");
  hPrintf("<tr><td> zoom in 3x</td><td class=\"hotkey\">i</td>        <td> reverse</td><td class=\"hotkey\">r then v</td>                    </tr>\n");
  hPrintf("<tr><td> zoom in 10x</td><td class=\"hotkey\">I</td>       <td> resize</td><td class=\"hotkey\">r then s</td>                     </tr>\n");
  hPrintf("<tr><td> zoom in base level</td><td class=\"hotkey\">b</td><td> refresh</td><td class=\"hotkey\">r then f</td>                    </tr>\n");
- hPrintf("<tr><td> zoom out 1.5x</td><td class=\"hotkey\">ctrl+k</td><td> jump to position box</td><td class=\"hotkey\">/</td>              </tr>\n");
+ hPrintf("<tr><td> zoom out 1.5x</td><td class=\"hotkey\">ctrl+k</td><td> view chrom names</td><td class=\"hotkey\">v then s</td><td class='hotkey'></td>              </tr>\n");
  hPrintf("<tr><td> zoom out 3x</td><td class=\"hotkey\">k</td>");
  if (gotExtTools)
      hPrintf("<td>send to external tool</td><td class=\"hotkey\">s then t</td>");
  hPrintf("               </tr>\n");
  hPrintf("<tr><td> zoom out 10x</td><td class=\"hotkey\">K</td>      <td> exon view</td><td class=\"hotkey\">e then v</td>                  </tr>\n");
  hPrintf("<tr><td> zoom out 100x</td><td class=\"hotkey\">0</td>     <td> default view</td><td class=\"hotkey\">d then v</td>               </tr>\n");
  hPrintf("<tr><td> zoom to ...</td><td class=\"hotkey\"></td><td> view DNA</td><td class='hotkey'>v then d</td></tr>\n");
  hPrintf("<tr><td> &nbsp;50bp (1 zero)</td><td class=\"hotkey\">1</td><td>Reset all User Settings</td><td class='hotkey'>c then r</td></tr>\n");
  hPrintf("<tr><td> &nbsp;500bp (2 zeros)</td><td class=\"hotkey\">2</td><td>Tools - BLAT</td><td class='hotkey'>t then b</td></tr>\n");
  hPrintf("<tr><td> &nbsp;5000bp (3 zeros)</td><td class=\"hotkey\">3</td><td>Tools - Table Browser</td><td class='hotkey'>t then t</td></tr>\n");
  hPrintf("<tr><td> &nbsp;50kbp (4 zeros)</td><td class=\"hotkey\">4</td><td>Tools - PCR</td><td class='hotkey'>t then i</td></tr>\n");
  hPrintf("<tr><td> &nbsp;500kbp (5 zeros)</td><td class=\"hotkey\">5</td><td>My Sessions</td><td class='hotkey'>s then s</td></tr>\n");
  hPrintf("<tr><td> &nbsp;5Mbp (6 zeros)</td><td class=\"hotkey\">6</td><td>Public Sessions</td><td class='hotkey'>p then s</td></tr>\n");
  hPrintf("<tr><td>Highlight all (mark)</td><td class=\"hotkey\">h then m</td><td>Clear all Highlights</td><td class='hotkey'>h then c</td></tr>\n");
- hPrintf("<tr><td>View chrom names</td><td class=\"hotkey\">v then s</td><td></td><td class='hotkey'></td></tr>\n");
  hPrintf("</table>\n");
  hPrintf("<img style=\"margin:8px\" src=\"../images/shortcutHelp.png\">");
  hPrintf("</div>\n");
  }
  
  static void checkAddHighlight()
  /* If the cart variable addHighlight is set, merge it into the highlight variable. */
  {
  char *newHighlight = cartOptionalString(cart, "addHighlight");
  if (newHighlight)
      {
      char *existing = cartOptionalString(cart, "highlight");
      if (isNotEmpty(existing))
          {
          // Add region only if it is not already in the existing highlight setting.
          char *alreadyIn = strstr(existing, newHighlight);
          int len = strlen(newHighlight);
          if (! (alreadyIn && (alreadyIn[len] == '|' || alreadyIn[len] == '\0')))
              {
              struct dyString *dy = dyStringCreate("%s|%s", newHighlight, existing);
              cartSetString(cart, "highlight", dy->string);
              }
          }
      else
          cartSetString(cart, "highlight", newHighlight);
      cartRemove(cart, "addHighlight");
      }
  }
  
+ extern boolean issueBotWarning;
+ 
  void doMiddle(struct cart *theCart)
  /* Print the body of an html file.   */
  {
  cart = theCart;
  measureTiming = hPrintStatus() && isNotEmpty(cartOptionalString(cart, "measureTiming"));
  if (measureTiming)
-     measureTime("Startup");
+     measureTime("Startup (bottleneck %d ms) ", botDelayMillis);
  
- hgBotDelayFrac(0.25); /* Impose a quarter of the standard CGI penalty */
- if (measureTiming)
-     measureTime("Bottleneck delay");
+ if (issueBotWarning)
+     {
+     char *ip = getenv("REMOTE_ADDR");
+     botDelayMessage(ip, botDelayMillis);
+     }
  
  char *debugTmp = NULL;
  /* Uncomment this to see parameters for debugging. */
  /* struct dyString *state = NULL; */
  /* Initialize layout and database. */
  if (measureTiming)
      measureTime("Get cart of %d for user:%s session:%s", theCart->hash->elCount,
  	    theCart->userId, theCart->sessionId);
  /* #if 1 this to see parameters for debugging. */
  /* Be careful though, it breaks if custom track
   * is more than 4k */
  #if  0
  state = cgiUrlString();
  printf("State: %s\n", state->string);
  #endif
  
  getDbAndGenome(cart, &database, &organism, oldVars);
  
  initGenbankTableNames(database);
  
  protDbName = hPdbFromGdb(database);
  debugTmp = cartUsualString(cart, "hgDebug", "off");
  if(sameString(debugTmp, "on"))
      hgDebug = TRUE;
  else
      hgDebug = FALSE;
  
  int timeout = cartUsualInt(cart, "udcTimeout", 300);
  if (udcCacheTimeout() < timeout)
      udcSetCacheTimeout(timeout);
  
  // tell UDC where to put its statistics file
  char *udcLogFile;
  if ((udcLogFile =  cfgOption("udcLog")) != NULL)
      {
      FILE *fp = mustOpen(udcLogFile, "a");
      udcSetLog(fp);
      }
  
  initTl();
  
  char *configPageCall = cartCgiUsualString(cart, "hgTracksConfigPage", "notSet");
  char *configMultiRegionPageCall = cartCgiUsualString(cart, "hgTracksConfigMultiRegionPage", "notSet");
  
  checkAddHighlight();
  
  /* Do main display. */
  
  if (cartUsualBoolean(cart, "hgt.trackImgOnly", FALSE))
      {
      trackImgOnly = TRUE;
      ideogramToo = cartUsualBoolean(cart, "hgt.ideogramToo", FALSE);
      hideControls = TRUE;
      withNextItemArrows = FALSE;
      withNextExonArrows = FALSE;
      hgFindMatches = NULL;     // XXXX necessary ???
      }
  
  jsonForClient = newJsonObject(newHash(8));
  jsonObjectAdd(jsonForClient, "cgiVersion", newJsonString(CGI_VERSION));
  boolean searching = differentString(cartUsualString(cart, TRACK_SEARCH,"0"), "0");
  
  if(!trackImgOnly)
      {
      // Write out includes for css and js files
      hWrites(commonCssStyles());
      jsIncludeFile("mousetrap.min.js", NULL);
      jsIncludeFile("jquery.js", NULL);
      jsIncludeFile("jquery-ui.js", NULL);
      jsIncludeFile("utils.js", NULL);
      jsIncludeFile("ajax.js", NULL);
      jsIncludeFile("jquery.watermarkinput.js", NULL);
      if(!searching)
          {
          jsIncludeFile("jquery.history.js", NULL);
          jsIncludeFile("jquery.imgareaselect.js", NULL);
          }
      jsIncludeFile("autocomplete.js", NULL);
      jsIncludeFile("es5-shim.4.0.3.min.js", NULL);
      jsIncludeFile("es5-sham.4.0.3.min.js", NULL);
      jsIncludeFile("lodash.3.10.0.compat.min.js", NULL);
      jsIncludeFile("autocompleteCat.js", NULL);
      jsIncludeFile("hgTracks.js", NULL);
      jsIncludeFile("spectrum.min.js", NULL);
  
  #ifdef LOWELAB
      jsIncludeFile("lowetooltip.js", NULL);
  #endif///def LOWELAB
  
      webIncludeResourceFile("spectrum.min.css");
      webIncludeResourceFile("jquery-ui.css");
      if (!searching)     // NOT doing search
          {
          webIncludeResourceFile("jquery.contextmenu.css");
          jsIncludeFile("jquery.contextmenu.js", NULL);
          webIncludeResourceFile("ui.dropdownchecklist.css");
          jsIncludeFile("ui.dropdownchecklist.js", NULL);
          jsIncludeFile("ddcl.js", NULL);
          }
  
      hPrintf("<div id='hgTrackUiDialog' style='display: none'></div>\n");
      hPrintf("<div id='hgTracksDialog' style='display: none'></div>\n");
  
      cartFlushHubWarnings();
      }
  
  if (cartVarExists(cart, "chromInfoPage"))
      {
      cartRemove(cart, "chromInfoPage");
      chromInfoPage();
      }
  else if (differentString(cartUsualString(cart, TRACK_SEARCH,"0"),"0"))
      {
      doSearchTracks(groupList);
      }
  else if (sameWord(configPageCall, "configure") ||
           sameWord(configPageCall, "configure tracks and display"))
      {
      cartRemove(cart, "hgTracksConfigPage");
      configPage();
      }
  else if (sameWord(configMultiRegionPageCall, "multi-region"))
      {
      cartRemove(cart, "hgTracksConfigMultiRegionPage");
      configMultiRegionPage();
      }
  else if (cartVarExists(cart, configHideAll))
      {
      cartRemove(cart, configHideAll);
      configPageSetTrackVis(tvHide);
      }
  else if (cartVarExists(cart, configShowAll))
      {
      cartRemove(cart, configShowAll);
      configPageSetTrackVis(tvDense);
      }
  else if (cartVarExists(cart, configDefaultAll))
      {
      cartRemove(cart, configDefaultAll);
      configPageSetTrackVis(-1);
      }
  else if (cartVarExists(cart, configHideAllGroups))
      {
      cartRemove(cart, configHideAllGroups);
      struct grp *grp = NULL, *grps = hLoadGrps(database);
      for (grp = grps; grp != NULL; grp = grp->next)
          collapseGroup(grp->name, TRUE);
      configPageSetTrackVis(-2);
      }
  else if (cartVarExists(cart, configShowAllGroups))
      {
      cartRemove(cart, configShowAllGroups);
      struct grp *grp = NULL, *grps = hLoadGrps(database);
      for (grp = grps; grp != NULL; grp = grp->next)
          collapseGroup(grp->name, FALSE);
      configPageSetTrackVis(-2);
      }
  else if (cartVarExists(cart, configHideEncodeGroups))
      {
      /* currently not used */
      cartRemove(cart, configHideEncodeGroups);
      struct grp *grp = NULL, *grps = hLoadGrps(database);
      for (grp = grps; grp != NULL; grp = grp->next)
          if (startsWith("encode", grp->name))
              collapseGroup(grp->name, TRUE);
      configPageSetTrackVis(-2);
      }
  else if (cartVarExists(cart, configShowEncodeGroups))
      {
      /* currently not used */
      cartRemove(cart, configShowEncodeGroups);
      struct grp *grp = NULL, *grps = hLoadGrps(database);
      for (grp = grps; grp != NULL; grp = grp->next)
          if (startsWith("encode", grp->name))
              collapseGroup(grp->name, FALSE);
      configPageSetTrackVis(-2);
      }
  else
      {
      tracksDisplay();
      }
  
  if (cartVarExists(cart, "hgt.convertChromToVirtChrom"))
      {
      cartRemove(cart, "hgt.convertChromToVirtChrom");
      return;
      }
  
  jsonObjectAdd(jsonForClient, "measureTiming", newJsonBoolean(measureTiming));
  // js code needs to know if a highlightRegion is defined for this db
  checkAddHighlight(); // call again in case tracksDisplay's call to resolvePosition changed vars
  char *highlightDef = cartOptionalString(cart, "highlight");
  if (highlightDef && startsWith(database,highlightDef) && highlightDef[strlen(database)] == '.')
      jsonObjectAdd(jsonForClient, "highlight", newJsonString(highlightDef));
  jsonObjectAdd(jsonForClient, "enableHighlightingDialog",
  	      newJsonBoolean(cartUsualBoolean(cart, "enableHighlightingDialog", TRUE)));
  
  struct dyString *dy = dyStringNew(1024);
  jsonDyStringPrint(dy, (struct jsonElement *) jsonForClient, "hgTracks", 0);
  jsInline(dy->string);
  dyStringFree(&dy);
  
  if (measureTiming)
      measureTime("Time at end of doMiddle, next up cart write");
  
  if (cartOptionalString(cart, "udcTimeout"))
      {
      warn("The Genome Browser cart currently includes the \"udcTimeout\" string. "
  	"While this is useful for debugging hubs, it may negatively impact "
  	"performance.   To clear this variable, click "
  	"<A HREF='hgTracks?hgsid=%s|url|&udcTimeout=[]'>here</A>.",cartSessionId(cart));
      }
  }
  
  void labelTrackAsFiltered(struct track *tg)
  /* add text to track long label to indicate filter is active */
  {
  char *oldLabel = tg->longLabel;
  tg->longLabel = catTwoStrings(oldLabel, " (filter activated)");
- freeMem(oldLabel);
  }