/* hui - human genome user interface common controls. */

/* Copyright (C) 2014 The Regents of the University of California 
* See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */

#include "common.h"
#include "hash.h"
#include "cheapcgi.h"
#include "htmshell.h"
#include "jksql.h"
#include "jsHelper.h"
#include "sqlNum.h"
#include "cart.h"
#include "hdb.h"
#include "hui.h"
#include "hCommon.h"
#include "hgConfig.h"
#include "chainCart.h"
#include "chainDb.h"
#include "netCart.h"
#include "obscure.h"
#include "wiggle.h"
#include "phyloTree.h"
#include "hgMaf.h"
#include "udc.h"
#include "customTrack.h"
#include "encode/encodePeak.h"
#include "mdb.h"
#include "web.h"
#include "hPrint.h"
#include "fileUi.h"
#include "bigBed.h"
#include "bigRmskUi.h"
#include "bigWig.h"
#include "regexHelper.h"
#include "snakeUi.h"
#include "vcfUi.h"
#include "vcf.h"
#include "errCatch.h"
#include "samAlignment.h"
#include "makeItemsItem.h"
#include "bedDetail.h"
#include "pgSnp.h"
#include "memgfx.h"
#include "trackHub.h"
#include "gtexUi.h"
#include "genbank.h"
#include "htmlPage.h"
#include "longRange.h"
#include "barChartUi.h"
#include "interactUi.h"
#include "interact.h"
#include "hicUi.h"
#include "bigDbSnp.h"
#include "customComposite.h"
#include "trackVersion.h"
#include "hubConnect.h"
#include "bigBedFilter.h"

// TODO: these should go away after refactoring of multi-region link
#include "hex.h"
#include "net.h"
#include "trashDir.h"
#include <openssl/sha.h>

#define SMALLBUF 256
#define MAX_SUBGROUP 9
#define ADD_BUTTON_LABEL        "add"
#define CLEAR_BUTTON_LABEL      "clear"
#define JBUFSIZE 2048


#define DEF_BUTTON "<IMG id=\"btn_%s\" src=\"../images/%s\" alt=\"%s\">\n"
#define DEF_BUTTON_JS "setCheckBoxesThatContain('%s',true,false,'%s','','%s');" \
	       "setCheckBoxesThatContain('%s',false,false,'%s','_defOff','%s');" 
#define DEFAULT_BUTTON(nameOrId,anc,beg,contains) \
    printf(DEF_BUTTON,(anc),"defaults_sm.png","default"); \
    safef(id, sizeof id, "btn_%s", (anc)); \
    jsOnEventByIdF("click", id, DEF_BUTTON_JS,(nameOrId),(beg),(contains),(nameOrId),(beg),(contains)); 

#define PM_BUTTON  "<IMG height=18 width=18 id=\"btn_%s\" src=\"../images/%s\" alt=\"%s\">\n"
#define PM_BUTTON_JS  "setCheckBoxesThatContain('%s',%s,true,'%s','','%s');"
#define PLUS_BUTTON(nameOrId,anc,beg,contains) \
    printf(PM_BUTTON, (anc), "add_sm.gif",   "+"); \
    safef(id, sizeof id, "btn_%s", (anc)); \
    jsOnEventByIdF("click", id, PM_BUTTON_JS, (nameOrId),"true", (beg),(contains)); 
#define MINUS_BUTTON(nameOrId,anc,beg,contains) \
    printf(PM_BUTTON, (anc), "remove_sm.gif", "-"); \
    safef(id, sizeof id, "btn_%s", (anc)); \
    jsOnEventByIdF("click", id, PM_BUTTON_JS, (nameOrId),"false", (beg),(contains)); 

boolean isEncode2(char *database, char *track)
/* Return true for tracks created by UCSC DCC during ENCODE production phase */
{
if (startsWith("wgEncode", track))
    return (sameString(database, "hg18") || sameString(database, "hg19") || 
                sameString(database, "mm9"));
return FALSE;
}

static char *htmlStringForDownloadsLink(char *database, struct trackDb *tdb,
				    char *name,boolean nameIsFile)
// Returns an HTML string for a downloads link
{
// If has fileSortOrder, then link to new hgFileUi
if (!nameIsFile && trackDbSetting(tdb, FILE_SORT_ORDER) != NULL)
    {
    char * link = needMem(PATH_LEN); // 512 should be enough
    safef(link,PATH_LEN,"<A HREF='%s?db=%s&g=%s' title='Downloadable Files...' TARGET='ucscDownloads'>%s</A>",
	  hgFileUiName(),database, /*cartSessionVarName(),cartSessionId(cart),*/ tdb->track, name);
	  // Note the hgsid would be needed if downloads page ever saved fileSortOrder to cart.
    return link;
    }
else if (trackDbSetting(tdb, "wgEncode") != NULL && isEncode2(database, tdb->track))  // Downloads directory if this is ENCODE
    {
    const char *compositeDir = metadataFindValue(tdb, MDB_OBJ_TYPE_COMPOSITE);
    if (compositeDir == NULL && tdbIsComposite(tdb))
	compositeDir = tdb->track;
    if (compositeDir != NULL)
	{
	struct dyString *dyLink =
		dyStringCreate("<A HREF=\"http://%s/goldenPath/%s/%s/%s/%s\" title='Download %s' "
			       "class='file' TARGET=ucscDownloads>%s</A>",
			       hDownloadsServer(), database, ENCODE_DCC_DOWNLOADS, compositeDir,
			       (nameIsFile?name:""), nameIsFile?"file":"files",name);
	return dyStringCannibalize(&dyLink);
	}
    }
return NULL;
}

static boolean makeNamedDownloadsLink(char *database, struct trackDb *tdb,char *name)
// Make a downloads link (if appropriate and then returns TRUE)
{
char *htmlString = htmlStringForDownloadsLink(database,trackDbTopLevelSelfOrParent(tdb),name,FALSE);
if (htmlString == NULL)
    return FALSE;

printf("%s", htmlString);
freeMem(htmlString);
return TRUE;
}

boolean makeDownloadsLink(char *database, struct trackDb *tdb)
// Make a downloads link (if appropriate and then returns TRUE)
{
return makeNamedDownloadsLink(database, tdb,"Downloads");
}

void makeTopLink(struct trackDb *tdb)
// Link to top of UI page
{
if (trackDbSetting(tdb, "dimensions"))
    {
    char *upArrow = "&uArr;";
    enum browserType browser = cgiBrowser();
    if (browser == btIE || browser == btFF)
	upArrow = "&uarr;";
    // Note: the nested spans are so that javascript can determine position
    // and selectively display the link when appropriate
    printf("<span class='navUp' style='float:right; display:none'>&nbsp;&nbsp;"
	   "<A HREF='#' TITLE='Return to top of page'>Top%s</A></span>",upArrow);
    }
}

boolean makeSchemaLink(char *db,struct trackDb *tdb,char *label)
// Make a table schema link (if appropriate and then returns TRUE)
{
#define SCHEMA_LINKED "<A HREF=\"../cgi-bin/hgTables?db=%s&hgta_group=%s&hgta_track=%s" \
		  "&hgta_table=%s&hgta_doSchema=describe+table+schema\" " \
		  "TARGET=ucscSchema%s>%s</A>"
if (trackDataAccessible(db, tdb) && differentString("longTabix", tdb->type))
    // FIXME: hgTables.showSchmaLongTabix is a currently a dummy routine, so let's not got here
    // until it's implemented
    {
    char *tbOff = trackDbSetting(tdb, "tableBrowser");
    if (isNotEmpty(tbOff) && sameString(nextWord(&tbOff), "off"))
	return FALSE;
    char *hint = " title='Open data format (table schema) in new window'";
    if (label == NULL)
	label = " View data format";
    struct trackDb *topLevel = trackDbTopLevelSelfOrParent(tdb);
    printf(SCHEMA_LINKED, db, topLevel->grp, topLevel->track, tdb->table, hint, label);
    return TRUE;
    }
return FALSE;
}

char *wgEncodeVocabLink(char *file,char *term,char *value,char *title, char *label,char *suffix)
// returns allocated string of HTML link to controlled vocabulary term
{
#define VOCAB_LINK_WITH_FILE "<A HREF='hgEncodeVocab?ra=%s&%s=\"%s\"' title='%s details' " \
			 "class='cv' TARGET=ucscVocab>%s</A>"
#define VOCAB_LINK "<A HREF='hgEncodeVocab?%s=\"%s\"' title='%s details' class='cv' " \
	       "TARGET=ucscVocab>%s</A>"
struct dyString *dyLink = NULL;
char *encTerm = cgiEncode(term);
char *encValue = cgiEncode(value);
if (file != NULL)
    {
    char *encFile = cgiEncode(file);
    dyLink = dyStringCreate(VOCAB_LINK_WITH_FILE,encFile,encTerm,encValue,title,label);
    freeMem(encFile);
    }
else
    dyLink = dyStringCreate(VOCAB_LINK,encTerm,encValue,title,label);
if (suffix != NULL)
    dyStringAppend(dyLink,suffix);  // Don't encode since this may contain HTML

freeMem(encTerm);
freeMem(encValue);
return dyStringCannibalize(&dyLink);
}

char *pairsAsHtmlTable( struct slPair *pairs, struct trackDb *tdb, boolean showLongLabel,boolean showShortLabel)
/* Return a string which is an HTML table of the tags for this track. */
{
if (pairs == NULL)
    return "";

struct dyString *dyTable = dyStringCreate("<table style='display:inline-table;'>");

if (showLongLabel)
    dyStringPrintf(dyTable,"<tr valign='bottom'><td colspan=2 nowrap>%s</td></tr>",tdb->longLabel);
if (showShortLabel)
    dyStringPrintf(dyTable,"<tr valign='bottom'><td align='right' nowrap><i>shortLabel:</i></td>"
			   "<td nowrap>%s</td></tr>",tdb->shortLabel);

for(; pairs; pairs = pairs->next)
    {
    if (!sameString(pairs->name, "meta")  && !isEmpty((char *)pairs->val))
        dyStringPrintf(dyTable,"<tr valign='bottom'><td align='right' nowrap><i>%s:</i></td>"
                           "<td nowrap>%s</td></tr>",pairs->name, (char *)pairs->val);
    }
dyStringAppend(dyTable,"</table>");
return dyStringCannibalize(&dyTable);
}

char *metadataAsHtmlTable(char *db,struct trackDb *tdb,boolean showLongLabel,boolean showShortLabel)
// If metadata from metaDb exists, return string of html with table definition
{
struct slPair *pairs = NULL;

if ((pairs = trackDbMetaPairs(tdb)) != NULL)
    return pairsAsHtmlTable(pairs, tdb, showLongLabel, showShortLabel);

const struct mdbObj *safeObj = metadataForTable(db,tdb,NULL);
if (safeObj == NULL || safeObj->vars == NULL)
    return NULL;

//struct dyString *dyTable = dyStringCreate("<table id='mdb_%s'>",tdb->table);
struct dyString *dyTable = dyStringCreate("<table style='display:inline-table;'>");
if (showLongLabel)
    dyStringPrintf(dyTable,"<tr valign='bottom'><td colspan=2 nowrap>%s</td></tr>",tdb->longLabel);
if (showShortLabel)
    dyStringPrintf(dyTable,"<tr valign='bottom'><td align='right' nowrap><i>shortLabel:</i></td>"
			   "<td nowrap>%s</td></tr>",tdb->shortLabel);

// Get the hash of mdb and cv term types
struct hash *cvTermTypes = (struct hash *)cvTermTypeHash();

struct mdbObj *mdbObj = mdbObjClone(safeObj); // Important if we are going to remove vars!
// Don't bother showing these
mdbObjRemoveVars(mdbObj,MDB_OBJ_TYPE_COMPOSITE " " MDB_VAR_PROJECT " " MDB_OBJ_TYPE " "
		    MDB_VAR_MD5SUM);
mdbObjRemoveHiddenVars(mdbObj);
mdbObjReorderByCv(mdbObj,FALSE);// Use cv defined order for visible vars
struct mdbVar *mdbVar;
for (mdbVar=mdbObj->vars;mdbVar!=NULL;mdbVar=mdbVar->next)
    {
    if ((sameString(mdbVar->var,MDB_VAR_FILENAME) || sameString(mdbVar->var,MDB_VAR_FILEINDEX) )
    && trackDbSettingClosestToHome(tdb,MDB_VAL_ENCODE_PROJECT) != NULL)
	{
	dyStringPrintf(dyTable,"<tr valign='top'><td align='right' nowrap><i>%s:</i></td>"
			       "<td nowrap>",mdbVar->var);

	struct slName *fileSet = slNameListFromComma(mdbVar->val);
	while (fileSet != NULL)
	    {
	    struct slName *file = slPopHead(&fileSet);
	    dyStringAppend(dyTable,htmlStringForDownloadsLink(db, tdb, file->name, TRUE));
	    if (fileSet != NULL)
		dyStringAppend(dyTable,"<BR>");
	    slNameFree(&file);
	    }
	dyStringAppend(dyTable,"</td></tr>");
	}
    else
	{                                           // Don't bother with tableName
	if (cvTermTypes && differentString(mdbVar->var,MDB_VAR_TABLENAME))
	    {
	    struct hash *cvTerm = hashFindVal(cvTermTypes,mdbVar->var);
	    if (cvTerm != NULL) // even if cvTerm isn't used,
		{               // it proves that it exists and a link is desirable
		if (!cvTermIsHidden(mdbVar->var))
		    {
		    char *label = (char *)cvLabel(NULL,mdbVar->var);
		    char *linkOfType = wgEncodeVocabLink(NULL,CV_TYPE,mdbVar->var,label,
							   label,NULL);
		    if (cvTermIsCvDefined(mdbVar->var))
			{
			label = (char *)cvLabel(mdbVar->var,mdbVar->val);
			char *linkOfTerm = wgEncodeVocabLink(NULL,CV_TERM,mdbVar->val,label,
							       label,NULL);
			dyStringPrintf(dyTable,"<tr valign='bottom'><td align='right' nowrap>"
					       "<i>%s:</i></td><td nowrap>%s</td></tr>",
					       linkOfType,linkOfTerm);
			freeMem(linkOfTerm);
			}
		    else
			dyStringPrintf(dyTable,"<tr valign='bottom'><td align='right' nowrap>"
					       "<i>%s:</i></td><td nowrap>%s</td></tr>",
					       linkOfType,mdbVar->val);
		    freeMem(linkOfType);
		    continue;
		    }
		}
	    }
	dyStringPrintf(dyTable,"<tr valign='bottom'><td align='right' nowrap><i>%s:</i></td>"
			       "<td nowrap>%s</td></tr>",mdbVar->var,mdbVar->val);
	}
    }
dyStringAppend(dyTable,"</table>");
//mdbObjsFree(&mdbObj); // spill some memory
return dyStringCannibalize(&dyTable);
}

boolean compositeMetadataToggle(char *db,struct trackDb *tdb,char *title,
    boolean embeddedInText,boolean showLongLabel)
// If metadata from metaTbl exists, create a link that will allow toggling it's display
{
boolean hasMetaInHub = (trackDbSetting(tdb, "metaDb") != NULL) ||  (trackDbSetting(tdb, "metaTab") != NULL);
if (!hasMetaInHub)
    {
    const struct mdbObj *safeObj = metadataForTable(db,tdb,NULL);
    if (safeObj == NULL || safeObj->vars == NULL)
        return FALSE;
    }

char id[256];
safef(id, sizeof id, "div_%s_link", tdb->track);
printf("%s<A id='%s' HREF='#a_meta_%s' "
   "title='Show metadata details...'>%s<img src='../images/downBlue.png'/></A>",
   (embeddedInText?"&nbsp;":"<P>"),id,tdb->track, (title?title:""));
jsOnEventByIdF("click", id, "return metadataShowHide(\"%s\",%s,true);", 
    tdb->track, showLongLabel?"true":"false");
printf("<DIV id='div_%s_meta' style='display:none;'>%s</div>",tdb->track, metadataAsHtmlTable(db,tdb,showLongLabel,FALSE));
return TRUE;
}

/* Multi-region UI */

boolean makeMultiRegionLink(char *db, struct trackDb *tdb, struct cart *cart)
/* Make a link to launch browser in multi-region custom URL mode, based on
 * track setting. This includes creating a custom track displaying the regions.
 * The link switches to exit multi-region if browser is already in multi-region mode
 * based on regions defined for this track. */
{
char *regionUrl = trackDbSetting(tdb, "multiRegionsBedUrl");
if (isEmpty(regionUrl))
    return FALSE;

// make custom track for regions, alternating colors

// TODO: truncate CT name and label at word boundary
// TODO: fix bedPackDense to work with multi-region
// TODO: limit number of regions ?
struct dyString *dsCustomText = dyStringCreate(
            "track name=\'%s ROI\' description=\'[Regions of Interest] %s' "
            "visibility=dense bedPackDense=on labelOnFeature=on itemRgb=on noScoreFilter=on\n",
                tdb->shortLabel, tdb->longLabel);

#ifdef LATER
// TODO: libify
struct dyString *ds = NULL;
struct errCatch *errCatch = errCatchNew();
if (errCatchStart(errCatch))
    {
    int sd = netUrlOpen(regionUrl);
    if (sd >= 0)
        {
        char *newUrl = NULL;
        int newSd = 0;
        if (netSkipHttpHeaderLinesHandlingRedirect(sd, regionUrl, &newSd, &newUrl))
            {
            if (newUrl) /* redirect can modify the url */
                {
                freeMem(newUrl);
                sd = newSd;
                }

            ds = netSlurpFile(sd);
            close(sd);
            }
        }
    }
errCatchEnd(errCatch);
if (errCatch->gotError)
    warn("%s", errCatch->message->string);

// how come no warning if bad file ?
errCatchFree(&errCatch);
#endif

// TODO: support $D, etc. in URL
int sd = netUrlOpen(regionUrl);
if (sd < 0)
    return FALSE;
struct dyString *dsRegionBed = netSlurpFile(sd);
close(sd);
if (!dsRegionBed)
    return FALSE;
char *regionBedTxt = dyStringCannibalize(&dsRegionBed);

// count fields in BED. Accept up to BED9 (user-spec colors)
char *bedTxt = cloneString(regionBedTxt);
struct lineFile *lf = lineFileOnString(NULL, TRUE, bedTxt);
char *words[9];
int bedSize = lineFileChopNext(lf, words, sizeof words);
lineFileClose(&lf);
freeMem(bedTxt);

lf = lineFileOnString(NULL, TRUE, regionBedTxt);

// TODO: refactor with interact multi-region

static char *colorLight = "184,201,255";       // blue
static char *colorDark = "0,0,0";              // black
char *color = colorLight;
boolean doLightColor = TRUE;

int id = 1;
char name[100];
char userColor[10];
struct bed *region;

struct tempName mrTn;
trashDirFile(&mrTn, "hgt", "custRgn_track", ".bed");
FILE *f = fopen(mrTn.forCgi, "w");
if (f == NULL)
    errAbort("can't create temp file %s", mrTn.forCgi);
char regionInfo[1024];
char *regionFile = cloneString(mrTn.forCgi);

// TODO: trackDb setting ?
#define MULTI_REGION_BED_DEFAULT_PADDING        1000
int padding = MULTI_REGION_BED_DEFAULT_PADDING;
safef(regionInfo, sizeof regionInfo, "#padding %d\n", padding);
mustWrite(f, regionInfo, strlen(regionInfo));

#ifdef LATER
safef(regionInfo, sizeof regionInfo, "#shortDesc %s\n", name);
mustWrite(f, regionInfo, strlen(regionInfo));
#endif

// write to trash file and custom track
int regionCount = 0;
while (lineFileChopNext(lf, words, bedSize))
    {
    region = bedLoadN(words, bedSize);
    if (bedSize < 9)
        {
        // assign alternating light/dark color
        color = doLightColor ? colorLight : colorDark;
        doLightColor = !doLightColor;
        }
    else
        {
        struct rgbColor rgb = bedColorToRgb(region->itemRgb);
        safef(userColor, sizeof userColor, "%d,%d,%d", rgb.r, rgb.g, rgb.b);
        }
    if (bedSize < 4)
        {
        // region label based on chrom and an item number
        safef(name, sizeof name, "r%d/%s", id++, region->chrom);
        }
    else
        {
        strcpy(name, region->name);
        }
    // write to trash file
    safef(regionInfo, sizeof regionInfo, "%s\t%d\t%d\n",
                    region->chrom, region->chromStart, region->chromEnd);
    mustWrite(f, regionInfo, strlen(regionInfo));

    // write to custom track
    int start = max(region->chromStart - padding, 0);
    int end = min(region->chromEnd + padding, hChromSize(db, region->chrom));
    dyStringPrintf(dsCustomText, "%s\t%d\t%d\t%s\t"
                        "0\t.\t%d\t%d\t%s\n",
                            region->chrom, start, end,  name,
                            start, end, color);
    regionCount++;
    }
lineFileClose(&lf);
fclose(f);

// create SHA1 file; used to see if file has changed
unsigned char hash[SHA_DIGEST_LENGTH];
SHA1((const unsigned char *)regionInfo, strlen(regionInfo), hash);
char newSha1[(SHA_DIGEST_LENGTH + 1) * 2];
hexBinaryString(hash, SHA_DIGEST_LENGTH, newSha1, (SHA_DIGEST_LENGTH + 1) * 2);
char sha1File[1024];
safef(sha1File, sizeof sha1File, "%s.sha1", mrTn.forCgi);
f = mustOpen(sha1File, "w");
mustWrite(f, newSha1, strlen(newSha1));
carefulClose(&f);

char customHtml[1000];
safef(customHtml, sizeof customHtml, "<h2>Description</h2>\n"
    "<p>This custom track displays regions of interest for the "
    "<a href='../cgi-bin/hgTrackUi?db=%s&g=%s'><em>%s</em> track</a>.</p>",
        db, tdb->track, tdb->shortLabel);

// TODO: support #padding in custom regions file

enum trackVisibility vis =
                hTvFromString(cartUsualString(cart, tdb->track, hStringFromTv(tdb->visibility)));
if (vis == tvHide)
    vis = tvDense;

printf("<p>");
printf("<a href='../cgi-bin/hgTracks?"
                "virtMode=1&"
                "virtModeType=customUrl&"
                "%s=on&"
                "virtShortDesc=%s&"
                "multiRegionsBedUrl=%s&"
                "%s=%s&"
                "%s=%s&"
                "%s=%s'>"
        "Display regions of interest (%d)</a>",
                    MULTI_REGION_BED_WIN_FULL, tdb->track, cgiEncode(regionFile), tdb->track, 
                    hStringFromTv(vis),
                    CT_CUSTOM_DOC_TEXT_VAR, cgiEncode(customHtml),
                    CT_CUSTOM_TEXT_VAR, cgiEncode(dyStringCannibalize(&dsCustomText)), regionCount);
printf(" in multi-region view (custom regions mode)");
printf("&nbsp;&nbsp;&nbsp;");
printf("<a href=\"../goldenPath/help/multiRegionHelp.html\" target=_blank>(Help)</a>\n");
printf("</p>");
return TRUE;
}

static void printDownloadUrl(char *downloadUrl, char *database, char *track)
/* given a string <label><space><url>, print a nice download link */
{
char *parts[2];
int partCount = chopByWhiteRespectDoubleQuotes(downloadUrl, parts, 2);
if (partCount!=2)
    {
    puts("<b>Internal Error:</b> The downloadUrl trackDb statement needs exactly two arguments, the file type and the URL.");
    return;
    }
char* fileType = parts[0];
stripString(fileType, "\"");  // Remove double quotes, weird that chopByWhiteRespectDoubleQuotes doesn't do this
char* url = parts[1];
char *newUrl = replaceInUrl(url, "", NULL, database, "", 0, 0, track, FALSE, NULL);
printf("<br>Download: <a href=\"%s\">%s</a>", newUrl, fileType);
}

static void makeFileDownloads(struct trackDb *tdb, char *db) 
/* given either downloadUrl or downloadUrl.1/.2/... in trackDb, print links to these files.
 * File can be anywhere on the internet, useful e.g. for GTF files for gene tracks */
{
char *downloadUrl = trackDbSetting(tdb, "downloadUrl");
struct slName* tdbNames = trackDbSettingsWildMatch(tdb, "downloadUrl.*");
if (downloadUrl)
    printDownloadUrl(downloadUrl, db, tdb->track);

while (tdbNames != NULL)
    {
    struct slName *tdbName = slPopHead(&tdbNames);
    printDownloadUrl(trackDbSetting(tdb, tdbName->name), db, tdb->track);
    slNameFree(&tdbName);
    }
}

void extraUiLinks(char *db, struct trackDb *tdb, struct cart *cart)
// Show metadata, and downloads, schema links where appropriate
{
makeMultiRegionLink(db, tdb, cart);

struct slPair *pairs = trackDbMetaPairs(tdb);
if (pairs != NULL)
    printf("<b>Metadata:</b><br>%s\n", pairsAsHtmlTable( pairs, tdb, FALSE, FALSE));
else if (!tdbIsComposite(tdb) && !trackHubDatabase(db) && (metadataForTable(db, tdb, NULL) != NULL))
    printf("<b>Metadata:</b><br>%s\n", metadataAsHtmlTable(db, tdb, FALSE, FALSE));

boolean schemaLink = trackDataAccessible(db, tdb);
boolean downloadLink = (trackDbSetting(tdb, "wgEncode") != NULL && !tdbIsSuperTrack(tdb));
int links = 0;
if (schemaLink)
    links++;
if (downloadLink)
    links++;

if (links > 0)
    cgiDown(0.7);
if (links > 1)
    printf("<table><tr><td nowrap>View table: ");

if (schemaLink && differentString("longTabix", tdb->type) && !isCustomComposite(tdb))
    // FIXME: hgTables.showSchmaLongTabix is a currently a dummy routine, so let's not got here
    // until it's implemented
    {
    makeSchemaLink(db,tdb,(links > 1 ? "Data format":"Data schema/format description and download"));
    if (downloadLink)
	printf(", ");
    }

makeFileDownloads(tdb, db);

if (downloadLink)
    {
    // special case exception (hg18:NHGRI BiPs are in 7 different dbs but only hg18 has downloads):
    char *targetDb = trackDbSetting(tdb, "compareGenomeLinks");
    if (targetDb != NULL)
	{
	targetDb = cloneFirstWordByDelimiter(targetDb,'=');
	if (!startsWith("hg",targetDb))
	    freez(&targetDb);
	}
    if (targetDb == NULL)
	targetDb = cloneString(db);

    makeNamedDownloadsLink(targetDb, tdb, (links > 1 ? "downloads":"Downloads"));
    freez(&targetDb);
    }

if (links > 1)
    printf("</td></tr></table>");
}


char *hUserCookie()
/* Return our cookie name. */
{
return cfgOptionDefault("central.cookie", "hguid");
}

char *hDownloadsServer()
/* get the downloads server from hg.conf or the default */
{
return cfgOptionDefault("downloads.server", "hgdownload.soe.ucsc.edu");
}

void setUdcTimeout(struct cart *cart)
/* set the udc cache timeout */
{
int timeout = cartUsualInt(cart, "udcTimeout", 300);
if (udcCacheTimeout() < timeout)
    udcSetCacheTimeout(timeout);
}

void setUdcCacheDir()
/* set the path to the udc cache dir */
{
if (cfgOptionBooleanDefault("udc.useLocalDiskCache", TRUE))
    {
    char *cacheDir = getenv("UDC_CACHEDIR");
    if (isEmpty(cacheDir))
        cacheDir = cfgOptionDefault("udc.cacheDir", udcDefaultDir());
    udcSetDefaultDir(cacheDir);
    }
else
    udcDisableCache();
}

void setUdcOptions(struct cart *cart)
/* do udc setup: set timeout and resolver and anything else that requires cart or hg.conf */
{
setUdcTimeout(cart);

char *prots = cfgOption("resolvProts");
char *cmd = cfgOption("resolvCmd");

if (prots && cmd)
        udcSetResolver(prots, cmd);
}


char *wrapWhiteFont(char *s)
/* Write white font around s */
{
static char buf[256];
safef(buf, sizeof(buf), "<span style='color:#FFFFFF;'>%s</span>", s);
return buf;
}

char *hDocumentRoot()
/* get the path to the DocumentRoot, or the default */
{
return cfgOptionDefault("browser.documentRoot", DOCUMENT_ROOT);
}

char *hHelpFile(char *fileRoot)
/* Given a help file root name (e.g. "hgPcrResult" or "cutters"),
* prepend the complete help directory path and add .html suffix.
* Do not free the statically allocated result. */
{
static char helpName[PATH_LEN];
/* This cfgOption comes from Todd Lowe's hgTrackUi.c addition (r1.230): */
char *helpDir = cfgOption("help.html");
if (helpDir != NULL)
    safef(helpName, sizeof(helpName), "%s/%s.html", helpDir, fileRoot);
else
    safef(helpName, sizeof(helpName), "%s%s/%s.html", hDocumentRoot(), HELP_DIR, fileRoot);
return helpName;
}

char *hFileContentsOrWarning(char *file)
/* Return the contents of the html file, or a warning message.
* The file path may begin with hDocumentRoot(); if it doesn't, it is
* assumed to be relative and hDocumentRoot() will be prepended. */
{
if (isEmpty(file))
    return cloneString("<BR>Program Error: Empty file name for include file"
		       "<BR>\n");
char path[PATH_LEN];
char *docRoot = hDocumentRoot();
if (startsWith(docRoot, file))
    safecpy(path, sizeof path, file);
else
    safef(path, sizeof path, "%s/%s", docRoot, file);
if (! fileExists(path))
    {
    char message[1024];
    safef(message, sizeof(message), "<BR>Program Error: Missing file %s</BR>",
	  path);
    return cloneString(message);
    }
/* If the file is there but not readable, readInGulp will errAbort,
* but I think that is serious enough that errAbort is OK. */
char *result;
readInGulp(path, &result, NULL);
return result;
}

char *hCgiRoot()
/* get the path to the CGI directory.
* Returns NULL when not running as a CGI (unless specified by browser.cgiRoot) */
{
static char defaultDir[PATH_LEN];
char *scriptFilename = getenv("SCRIPT_FILENAME");
if (scriptFilename)
    {
    char dir[PATH_LEN], name[FILENAME_LEN], extension[FILEEXT_LEN];
    dir[0] = 0;
    splitPath(scriptFilename, dir, name, extension);
    safef(defaultDir, sizeof(defaultDir), "%s", dir);
    int len = strlen(defaultDir);
    // Get rid of trailing slash to be consistent with hDocumentRoot
    if (defaultDir[len-1] == '/')
	defaultDir[len-1] = 0;
    }
else
    {
    defaultDir[0] = 0;
    }
return cfgOptionDefault("browser.cgiRoot", defaultDir);
}

char *hTrackUiForTrack(char *trackName)
/* Relative URL to extended track UI, delegated to a track-specific UI if available. */
{
if (trackName == NULL)
    return hgTrackUiName();
if (gtexIsGeneTrack(trackName))
    return gtexTrackUiName();
if (gtexIsEqtlTrack(trackName))
    return gtexTrackUiName();
return hgTrackUiName();
}

/******  Some stuff for tables of controls ******/

struct controlGrid *startControlGrid(int columns, char *align)
/* Start up a control grid. */
{
struct controlGrid *cg;
AllocVar(cg);
cg->columns = columns;
cg->align = cloneString(align);
cg->rowOpen = FALSE;
return cg;
}

void controlGridEndRow(struct controlGrid *cg)
/* Force end of row. */
{
printf("</tr>");
cg->rowOpen = FALSE;
cg->columnIx = 0;
}

void controlGridStartCell(struct controlGrid *cg)
/* Start a new cell in control grid. */
{
if (cg->columnIx == cg->columns)
    controlGridEndRow(cg);
if (!cg->rowOpen)
    {
    printf("<tr>");
    cg->rowOpen = TRUE;
    }
if (cg->align)
    printf("<td align=%s>", cg->align);
else
    printf("<td>");
}

void controlGridEndCell(struct controlGrid *cg)
/* End cell in control grid. */
{
printf("</td>");
++cg->columnIx;
}

void endControlGrid(struct controlGrid **pCg)
/* Finish up a control grid. */
{
struct controlGrid *cg = *pCg;
if (cg != NULL)
    {
    int i;
    if (cg->columnIx != 0 && cg->columnIx < cg->columns)
	for( i = cg->columnIx; i <= cg->columns; i++)
	    printf("<td>&nbsp;</td>\n");
    if (cg->rowOpen)
	printf("</tr>\n");
    printf("</table>\n");
    freeMem(cg->align);
    freez(pCg);
    }
}

/******  Some stuff for hide/dense/full controls ******/

static char *hTvStrings[] =
/* User interface strings for track visibility controls. */
    {
    "hide",
    "dense",
    "full",
    "pack",
    "squish"
    };
#define hTvStringShowSameAsFull "show"

enum trackVisibility hTvFromStringNoAbort(char *s)
// Given a string representation of track visibility, return as equivalent enum.
{
int vis = stringArrayIx(s, hTvStrings, ArraySize(hTvStrings));
if (vis < 0)
    {
    if (sameString(hTvStringShowSameAsFull,s))
	return tvShow;  // Show is the same as full!
    vis = 0;  // don't generate bogus value on invalid input
    }
return vis;
}

enum trackVisibility hTvFromString(char *s)
// Given a string representation of track visibility, return as equivalent enum.
{
enum trackVisibility vis = hTvFromStringNoAbort(s);
if ((int)vis < 0)
    errAbort("Unknown visibility %s", s);
return vis;
}

char *hStringFromTv(enum trackVisibility vis)
// Given enum representation convert to string.
{
return hTvStrings[vis];
}

void hTvDropDownClassWithJavascript(char *varName, char *id, enum trackVisibility vis, boolean canPack,
				char *class, struct slPair *events)
// Make track visibility drop down for varName with style class
{
static char *noPack[] =
    {
    "hide",
    "dense",
    "full",
    };
static char *pack[] =
    {
    "hide",
    "dense",
    "squish",
    "pack",
    "full",
    };
static int packIx[] = {tvHide,tvDense,tvSquish,tvPack,tvFull};
if (canPack)
    cgiMakeDropListClassWithIdStyleAndJavascript(varName, id, pack, ArraySize(pack),
					   pack[packIx[vis]], class, TV_DROPDOWN_STYLE,
					   events);
else
    cgiMakeDropListClassWithIdStyleAndJavascript(varName, id, noPack, ArraySize(noPack),
					   noPack[vis], class, TV_DROPDOWN_STYLE, events);
}

void hTvDropDownClassVisOnlyAndExtra(char *varName, enum trackVisibility vis,
				 boolean canPack, char *class, char *visOnly, struct slPair *events)
// Make track visibility drop down for varName with style class, and potentially limited to visOnly
{
static char *denseOnly[] =
    {
    "hide",
    "dense",
    };
static char *squishOnly[] =
    {
    "hide",
    "squish",
    };
static char *packOnly[] =
    {
    "hide",
    "pack",
    };
static char *fullOnly[] =
    {
    "hide",
    "full",
    };
static char *noPack[] =
    {
    "hide",
    "dense",
    "full",
    };
static char *pack[] =
    {
    "hide",
    "dense",
    "squish",
    "pack",
    "full",
    };
static int packIx[] = {tvHide,tvDense,tvSquish,tvPack,tvFull};
if (visOnly != NULL)
    {
    int visIx = (vis > 0) ? 1 : 0;
    if (sameWord(visOnly,"dense"))
	cgiMakeDropListClassWithStyleAndJavascript(varName, denseOnly, ArraySize(denseOnly),
						   denseOnly[visIx],class,TV_DROPDOWN_STYLE, events);
    else if (sameWord(visOnly,"squish"))
	cgiMakeDropListClassWithStyleAndJavascript(varName, squishOnly,
						   ArraySize(squishOnly), squishOnly[visIx],
						   class, TV_DROPDOWN_STYLE, events);
    else if (sameWord(visOnly,"pack"))
	cgiMakeDropListClassWithStyleAndJavascript(varName, packOnly, ArraySize(packOnly),
						   packOnly[visIx], class, TV_DROPDOWN_STYLE, events);
    else if (sameWord(visOnly,"full"))
	cgiMakeDropListClassWithStyleAndJavascript(varName, fullOnly, ArraySize(fullOnly),
						   fullOnly[visIx], class, TV_DROPDOWN_STYLE, events);
    else                        /* default when not recognized */
	cgiMakeDropListClassWithStyleAndJavascript(varName, denseOnly, ArraySize(denseOnly),
						   denseOnly[visIx], class, TV_DROPDOWN_STYLE, events);
    }
else
    {
    if (canPack)
	cgiMakeDropListClassWithStyleAndJavascript(varName, pack, ArraySize(pack),
						   pack[packIx[vis]], class, TV_DROPDOWN_STYLE, events);
    else
	cgiMakeDropListClassWithStyleAndJavascript(varName, noPack, ArraySize(noPack),
						   noPack[vis], class, TV_DROPDOWN_STYLE, events);
    }
}

void hideShowDropDownWithClassAndExtra(char *varName, char * id, boolean show, char *class, struct slPair *events)
// Make hide/show dropdown for varName
{
static char *hideShow[] =
    {
    "hide",
    "show"
    };
cgiMakeDropListClassWithIdStyleAndJavascript(varName, id, hideShow, ArraySize(hideShow),
				       hideShow[show], class, TV_DROPDOWN_STYLE, events);
}


/****** Some stuff for stsMap related controls *******/

static char *stsMapOptions[] = 
    {
    "All Genetic",
    "Genethon",
    "Marshfield",
    "deCODE",
    "GeneMap 99",
    "Whitehead YAC",
    "Whitehead RH",
    "Stanford TNG",
    };

enum stsMapOptEnum smoeStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, stsMapOptions);
if (x < 0)
    errAbort("Unknown option %s", string);
return x;
}

char *smoeEnumToString(enum stsMapOptEnum x)
/* Convert from enum to string representation. */
{
return stsMapOptions[x];
}

void smoeDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, stsMapOptions, ArraySize(stsMapOptions),
    curVal);
}

/****** Some stuff for stsMapMouseNew related controls *******/

static char *stsMapMouseOptions[] = 
    {
    "All Genetic",
    "WICGR Genetic Map",
    "MGD Genetic Map",
    "RH",
    };

enum stsMapMouseOptEnum smmoeStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, stsMapMouseOptions);
if (x < 0)
    errAbort("Unknown option %s", string);
return x;
}

char *smmoeEnumToString(enum stsMapMouseOptEnum x)
/* Convert from enum to string representation. */
{
return stsMapMouseOptions[x];
}

void smmoeDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, stsMapMouseOptions, ArraySize(stsMapMouseOptions),
    curVal);
}

/****** Some stuff for stsMapRat related controls *******/

static char *stsMapRatOptions[] = 
    {
    "All Genetic",
    "FHHxACI",
    "SHRSPxBN",
    "RH",
    };

enum stsMapRatOptEnum smroeStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, stsMapRatOptions);
if (x < 0)
    errAbort("Unknown option %s", string);
return x;
}

char *smroeEnumToString(enum stsMapRatOptEnum x)
/* Convert from enum to string representation. */
{
return stsMapRatOptions[x];
}

void smroeDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, stsMapRatOptions, ArraySize(stsMapRatOptions),
    curVal);
}

/****** Some stuff for fishClones related controls *******/

static char *fishClonesOptions[] = 
    {
    "Fred Hutchinson CRC",
    "National Cancer Institute",
    "Sanger Centre",
    "Roswell Park Cancer Institute",
    "Cedars-Sinai Medical Center",
    "Los Alamos National Lab",
    "UC San Francisco",
    };

enum fishClonesOptEnum fcoeStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, fishClonesOptions);
if (x < 0)
    errAbort("Unknown option %s", string);
return x;
}

char *fcoeEnumToString(enum fishClonesOptEnum x)
/* Convert from enum to string representation. */
{
return fishClonesOptions[x];
}

void fcoeDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, fishClonesOptions, ArraySize(fishClonesOptions),
    curVal);
}

/****** Some stuff for recombRate related controls *******/

static char *recombRateOptions[] = 
    {
    "deCODE Sex Averaged Distances",
    "deCODE Female Distances",
    "deCODE Male Distances",
    "Marshfield Sex Averaged Distances",
    "Marshfield Female Distances",
    "Marshfield Male Distances",
    "Genethon Sex Averaged Distances",
    "Genethon Female Distances",
    "Genethon Male Distances",
    };

enum recombRateOptEnum rroeStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, recombRateOptions);
if (x < 0)
    errAbort("Unknown option %s", string);
return x;
}

char *rroeEnumToString(enum recombRateOptEnum x)
/* Convert from enum to string representation. */
{
return recombRateOptions[x];
}

void rroeDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, recombRateOptions, ArraySize(recombRateOptions),
    curVal);
}

/****** Some stuff for recombRateRat related controls *******/

static char *recombRateRatOptions[] = 
    {
    "SHRSPxBN Sex Averaged Distances",
    "FHHxACI Sex Averaged Distances",
    };

enum recombRateRatOptEnum rrroeStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, recombRateRatOptions);
if (x < 0)
    errAbort("Unknown option %s", string);
return x;
}

char *rrroeEnumToString(enum recombRateRatOptEnum x)
/* Convert from enum to string representation. */
{
return recombRateRatOptions[x];
}

void rrroeDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, recombRateRatOptions, ArraySize(recombRateRatOptions),
    curVal);
}

/****** Some stuff for recombRateMouse related controls *******/

static char *recombRateMouseOptions[] = 
    {
    "WI Genetic Map Sex Averaged Distances",
    "MGD Genetic Map Sex Averaged Distances",
    };

enum recombRateMouseOptEnum rrmoeStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, recombRateMouseOptions);
if (x < 0)
    errAbort("Unknown option %s", string);
return x;
}

char *rrmoeEnumToString(enum recombRateMouseOptEnum x)
/* Convert from enum to string representation. */
{
return recombRateMouseOptions[x];
}

void rrmoeDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, recombRateMouseOptions, ArraySize(recombRateMouseOptions),
    curVal);
}

/****** Some stuff for CGH NCI60 related controls *******/

static char *cghNci60Options[] = 
    {
    "Tissue Averages",
    "BREAST",
    "CNS",
    "COLON",
    "LEUKEMIA",
    "LUNG",
    "MELANOMA",
    "OVARY",
    "PROSTATE",
    "RENAL",
    "All Cell Lines",
    };

enum cghNci60OptEnum cghoeStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, cghNci60Options);
if (x < 0)
    errAbort("Unknown option %s", string);
return x;
}

char *cghoeEnumToString(enum cghNci60OptEnum x)
/* Convert from enum to string representation. */
{
return cghNci60Options[x];
}

void cghoeDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, cghNci60Options, ArraySize(cghNci60Options),
    curVal);
}

/****** Some stuff for nci60 related controls *******/

static char *nci60Options[] = 
    {
    "Tissue Averages",
    "All Cell Lines",
    "BREAST",
    "CNS",
    "COLON",
    "LEUKEMIA",
    "MELANOMA",
    "OVARIAN",
    "PROSTATE",
    "RENAL",
    "NSCLC",
    "DUPLICATE",
    "UNKNOWN"
    };

enum nci60OptEnum nci60StringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, nci60Options);
if (x < 0)
    errAbort("hui::nci60StringToEnum() - Unknown option %s", string);
return x;
}

char *nci60EnumToString(enum nci60OptEnum x)
/* Convert from enum to string representation. */
{
return nci60Options[x];
}

void nci60DropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, nci60Options, ArraySize(nci60Options),
    curVal);
}


/*** Control of base/codon coloring code: ***/

/* All options (parallel to enum baseColorDrawOpt): */
static char *baseColorDrawAllOptionLabels[] =
{
BASE_COLOR_DRAW_OFF_LABEL,
BASE_COLOR_DRAW_GENOMIC_CODONS_LABEL,
BASE_COLOR_DRAW_ITEM_CODONS_LABEL,
BASE_COLOR_DRAW_DIFF_CODONS_LABEL,
BASE_COLOR_DRAW_ITEM_BASES_CDS_LABEL,
BASE_COLOR_DRAW_DIFF_BASES_CDS_LABEL,
};
static char *baseColorDrawAllOptionValues[] =
{
BASE_COLOR_DRAW_OFF,
BASE_COLOR_DRAW_GENOMIC_CODONS,
BASE_COLOR_DRAW_ITEM_CODONS,
BASE_COLOR_DRAW_DIFF_CODONS,
BASE_COLOR_DRAW_ITEM_BASES,
BASE_COLOR_DRAW_DIFF_BASES,
};

/* Subset of options for tracks with CDS info but not item sequence: */
static char *baseColorDrawGenomicOptionLabels[] =
{
BASE_COLOR_DRAW_OFF_LABEL,
BASE_COLOR_DRAW_GENOMIC_CODONS_LABEL,
};
static char *baseColorDrawGenomicOptionValues[] =
{
BASE_COLOR_DRAW_OFF,
BASE_COLOR_DRAW_GENOMIC_CODONS,
};

/* Subset of options for tracks with aligned item sequence but not CDS: */
static char *baseColorDrawItemOptionLabels[] =
{
BASE_COLOR_DRAW_OFF_LABEL,
BASE_COLOR_DRAW_ITEM_BASES_NC_LABEL,
BASE_COLOR_DRAW_DIFF_BASES_NC_LABEL,
};
static char *baseColorDrawItemOptionValues[] =
{
BASE_COLOR_DRAW_OFF,
BASE_COLOR_DRAW_ITEM_BASES,
BASE_COLOR_DRAW_DIFF_BASES,
};

enum baseColorDrawOpt baseColorDrawOptStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, baseColorDrawAllOptionValues);
if (x < 0)
    errAbort("hui::baseColorDrawOptStringToEnum() - Unknown option %s", string);
return x;
}

static boolean baseColorGotCds(struct trackDb *tdb)
/* Return true if this track has CDS info according to tdb (or is genePred). */
{
boolean gotIt = FALSE;
char *setting = trackDbSetting(tdb, BASE_COLOR_USE_CDS);
if (isNotEmpty(setting))
    {
    if (sameString(setting, "all") || sameString(setting, "given") ||
	sameString(setting, "genbank") || startsWith("table", setting))
	gotIt = TRUE;
    else if (! sameString(setting, "none"))
	errAbort("trackDb for %s, setting %s: unrecognized value \"%s\".  "
		 "must be one of {none, all, given, genbank, table}.",
		 tdb->track, BASE_COLOR_USE_CDS, setting);
    }
else if (startsWith("genePred", tdb->type)  || startsWith("bigGenePred", tdb->type))
    gotIt = TRUE;
return gotIt;
}

static boolean baseColorGotSequence(struct trackDb *tdb)
/* Return true if this track has aligned sequence according to tdb. */
{
boolean gotIt = FALSE;
char *setting = trackDbSetting(tdb, BASE_COLOR_USE_SEQUENCE);
if (isNotEmpty(setting))
    {
    if (sameString(setting, "genbank") || sameString(setting, "seq") ||
	sameString(setting, "ss") || startsWith("extFile", setting) ||
	sameString(setting, "hgPcrResult") || sameString(setting, "nameIsSequence") ||
	sameString(setting, "seq1Seq2") || sameString(setting, "lfExtra") ||
	sameString(setting, "lrg") || sameString(setting, "2bit") ||
	startsWith("table ", setting) || startsWithWord("db", setting))
	gotIt = TRUE;
    else if (differentString(setting, "none"))
	errAbort("trackDb for %s, setting %s: unrecognized value \"%s\".  "
		 "must be one of {none, genbank, seq, ss, extFile, nameIsSequence, seq1Seq2,"
		 "hgPcrResult, lfExtra, lrg, 2bit, table <em>table</em>}.",
		 tdb->track, BASE_COLOR_USE_SEQUENCE, setting);
    }
return gotIt;
}

static void baseColorDropLists(struct cart *cart, struct trackDb *tdb, char *name)
/* draw the baseColor drop list options */
{
enum baseColorDrawOpt curOpt = baseColorDrawOptEnabled(cart, tdb);
char *curValue = baseColorDrawAllOptionValues[curOpt];
char var[512];
safef(var, sizeof(var), "%s." BASE_COLOR_VAR_SUFFIX, name);
boolean gotCds = baseColorGotCds(tdb);
boolean gotSeq = baseColorGotSequence(tdb);
if (gotCds && gotSeq)
    {
    puts("<P><B>Color track by codons or bases:</B>");
    cgiMakeDropListFull(var, baseColorDrawAllOptionLabels,
			baseColorDrawAllOptionValues,
			ArraySize(baseColorDrawAllOptionLabels),
			curValue, NULL, NULL);
    printf("<A HREF=\"%s\">Help on mRNA coloring</A><BR>",
	   CDS_MRNA_HELP_PAGE);
    }
else if (gotCds)
    {
    char buf[256];
    char *disabled = NULL;
    safef(buf, sizeof(buf), "codonColoringChanged('%s');", name);
    puts("<P><B>Color track by codons:</B>");
    cgiMakeDropListFull(var, baseColorDrawGenomicOptionLabels,
			baseColorDrawGenomicOptionValues,
			ArraySize(baseColorDrawGenomicOptionLabels),
			curValue, "change", buf);
    printf("<A HREF=\"%s\">Help on codon coloring</A><BR>",
	   CDS_HELP_PAGE);
    safef(buf, sizeof(buf), "%s.%s", name, CODON_NUMBERING_SUFFIX);
    if (curOpt == baseColorDrawOff)
        disabled = "disabled";
    printf("<br /><b><span id='%sCodonNumberingLabel' %s>Show codon numbering</b>:</span>\n", 
                name, curOpt == baseColorDrawOff ? "class='disabled'" : "");    cgiMakeCheckBoxMore(buf, cartUsualBooleanClosestToHome(cart, tdb, FALSE, CODON_NUMBERING_SUFFIX, TRUE), disabled);
    }
else if (gotSeq)
    {
    puts("<P><B>Color track by bases:</B>");
    cgiMakeDropListFull(var, baseColorDrawItemOptionLabels,
			baseColorDrawItemOptionValues,
			ArraySize(baseColorDrawItemOptionLabels),
			curValue, NULL, NULL);
    printf("<A HREF=\"%s\">Help on base coloring</A><BR>",
	   CDS_BASE_HELP_PAGE);
    }
}

void baseColorDrawOptDropDown(struct cart *cart, struct trackDb *tdb)
/* Make appropriately labeled drop down of options if any are applicable.*/
{
baseColorDropLists(cart, tdb, tdb->track);
}

static enum baseColorDrawOpt limitDrawOptForType(struct trackDb *tdb, enum baseColorDrawOpt drawOpt)
/* If tdb->type is genePred, but something fancier like mRNA codons is enabled because the setting
 * is coming from a view that also includes a PSL track, downgrade it to genomic codons to avoid
 * drawing problems caused by the inappropriate setting. #21194 */
{
if (startsWith("genePred", tdb->type) && drawOpt > baseColorDrawGenomicCodons)
    drawOpt = baseColorDrawGenomicCodons;
return drawOpt;
}

enum baseColorDrawOpt baseColorDrawOptEnabled(struct cart *cart,
					  struct trackDb *tdb)
/* Query cart & trackDb to determine what drawing mode (if any) is enabled. */
{
char *stringVal = NULL;
assert(cart);
assert(tdb);

/* trackDb can override default of OFF; cart can override trackDb. */
stringVal = trackDbSettingClosestToHomeOrDefault(tdb, BASE_COLOR_DEFAULT,
						  BASE_COLOR_DRAW_OFF);
stringVal = cartUsualStringClosestToHome(cart, tdb, FALSE, BASE_COLOR_VAR_SUFFIX,stringVal);

return limitDrawOptForType(tdb, baseColorDrawOptStringToEnum(stringVal));
}


/*** Control of fancy indel display code: ***/

static boolean tdbOrCartBoolean(struct cart *cart, struct trackDb *tdb,
			    char *settingName, char *defaultOnOff)
/* Query cart & trackDb to determine if a boolean variable is set. */
{
boolean alreadySet;
alreadySet = !sameString("off",trackDbSettingOrDefault(tdb, settingName, defaultOnOff));
alreadySet = cartUsualBooleanClosestToHome(cart, tdb, FALSE, settingName, alreadySet);
	 // NOTE: parentLevel=FALSE because tdb param already is at appropriate level
return alreadySet;
}

static boolean indelAppropriate(struct trackDb *tdb)
/* Return true if it makes sense to offer indel display options for tdb. */
{
return (tdb && (startsWith("psl", tdb->type) || startsWith("bigPsl", tdb->type) || 
    startsWithWord("chain", tdb->type) || startsWithWord("bigChain", tdb->type) ||
    sameString("bam", tdb->type) || sameString("lrg", tdb->track)));
}

static void indelEnabledByName(struct cart *cart, struct trackDb *tdb, char *name,
	      float basesPerPixel, boolean *retDoubleInsert, boolean *retQueryInsert,
	      boolean *retPolyA)
/* Query cart & trackDb to determine what indel display (if any) is enabled. Set
* basesPerPixel to 0.0 to disable check for zoom level.  */
{
struct trackDb *tdbLevel = tdb;
if (differentString(tdb->track, name) && tdb->parent != NULL)
    tdbLevel = tdb->parent;

boolean apropos = indelAppropriate(tdb);
if (apropos && (basesPerPixel > 0.0))
    {
    // check indel max zoom
    float showIndelMaxZoom = trackDbFloatSettingOrDefault(tdbLevel, "showIndelMaxZoom", -1.0);
    if ((showIndelMaxZoom >= 0)
	&& ((basesPerPixel > showIndelMaxZoom) || (showIndelMaxZoom == 0.0)))
	apropos = FALSE;
    }

if (retDoubleInsert)
    *retDoubleInsert = apropos && tdbOrCartBoolean(cart, tdbLevel, INDEL_DOUBLE_INSERT, "off");
if (retQueryInsert)
    *retQueryInsert = apropos && tdbOrCartBoolean(cart, tdbLevel, INDEL_QUERY_INSERT, "off");
if (retPolyA)
    *retPolyA = apropos && tdbOrCartBoolean(cart, tdbLevel, INDEL_POLY_A, "off");
}

void indelEnabled(struct cart *cart, struct trackDb *tdb, float basesPerPixel,
	      boolean *retDoubleInsert, boolean *retQueryInsert,
	      boolean *retPolyA)
/* Query cart & trackDb to determine what indel display (if any) is enabled. Set
* basesPerPixel to 0.0 to disable check for zoom level.  */
{
indelEnabledByName(cart,tdb,tdb->track,basesPerPixel,retDoubleInsert,retQueryInsert,retPolyA);
}

static void indelShowOptionsWithNameExt(struct cart *cart, struct trackDb *tdb, char *name,
				    char *queryTerm,
				    boolean includeDoubleInsert, boolean includePolyA)
/* Make HTML inputs for indel display options if any are applicable. */
{
if (indelAppropriate(tdb))
    {
    boolean showDoubleInsert, showQueryInsert, showPolyA;
    char var[512];
    indelEnabledByName(cart, tdb, name, 0.0, &showDoubleInsert, &showQueryInsert, &showPolyA);
    printf("<TABLE><TR><TD colspan=2><B>Alignment Gap/Insertion Display Options</B>");
    printf("&nbsp;<A HREF=\"%s\">Help on display options</A></TD></TR>\n<TR valign='top'><TD>",
	   INDEL_HELP_PAGE);
    if (includeDoubleInsert)
	{
	safef(var, sizeof(var), "%s.%s", name, INDEL_DOUBLE_INSERT);
	cgiMakeCheckBox(var, showDoubleInsert);
	printf("</TD><TD>Draw double horizontal lines when both genome and %s have "
	       "an insertion</TD></TR>\n<TR valign='top'><TD>", queryTerm);
	}
    safef(var, sizeof(var), "%s.%s", name, INDEL_QUERY_INSERT);
    cgiMakeCheckBox(var, showQueryInsert);
    printf("</TD><TD>Draw a vertical purple line for an insertion at the beginning or "
	   "end of the <BR>%s, orange for insertion in the middle of the %s</TD></TR>\n"
	   "<TR valign='top'><TD>", queryTerm, queryTerm);
    if (includePolyA)
	{
	safef(var, sizeof(var), "%s.%s", name, INDEL_POLY_A);
	/* We can highlight valid polyA's only if we have query sequence --
	 * so indelPolyA code piggiebacks on baseColor code: */
	if (baseColorGotSequence(tdb))
	    {
	    cgiMakeCheckBox(var, showPolyA);
	    printf("</TD><TD>Draw a vertical green line where %s has a polyA tail "
		   "insertion</TD></TR>\n", queryTerm);
	    }
	}
    printf("</TABLE>\n");
    }
}

static void indelShowOptionsWithName(struct cart *cart, struct trackDb *tdb, char *name)
/* Make HTML inputs for indel display options if any are applicable. */
{
indelShowOptionsWithNameExt(cart, tdb, name, "query", TRUE, TRUE);
}

void indelShowOptions(struct cart *cart, struct trackDb *tdb)
/* Make HTML inputs for indel display options if any are applicable. */
{
indelShowOptionsWithName(cart, tdb, tdb->track);
}

#define BAM_DEFAULT_SHOW_DIFF_BASES_MAX_ZOOM "100"

void bamAddBaseAndIndelSettings(struct trackDb *tdb)
/* Unless already set in tdb, add settings to enable base-level differences and indel display. */
{
struct hash *settings = tdb->settingsHash;
if (!hashLookup(settings, BASE_COLOR_USE_SEQUENCE))
    hashAdd(settings, BASE_COLOR_USE_SEQUENCE, cloneString("lfExtra"));
if (!hashLookup(settings, BASE_COLOR_DEFAULT))
    hashAdd(settings, BASE_COLOR_DEFAULT, cloneString(BASE_COLOR_DRAW_DIFF_BASES));
if (!hashLookup(settings, SHOW_DIFF_BASES_ALL_SCALES))
    hashAdd(settings, SHOW_DIFF_BASES_ALL_SCALES, cloneString("."));
if (!hashLookup(settings, INDEL_DOUBLE_INSERT))
    hashAdd(settings, INDEL_DOUBLE_INSERT, cloneString("on"));
if (!hashLookup(settings, INDEL_QUERY_INSERT))
    hashAdd(settings, INDEL_QUERY_INSERT, cloneString("on"));
if (!hashLookup(settings, INDEL_POLY_A))
    hashAdd(settings, INDEL_POLY_A, cloneString("on"));
if (!hashLookup(settings, "showDiffBasesMaxZoom"))
    hashAdd(settings, "showDiffBasesMaxZoom", cloneString(BAM_DEFAULT_SHOW_DIFF_BASES_MAX_ZOOM));
}

/****** base position (ruler) controls *******/

static char *zoomOptions[] = 
    {
    ZOOM_1PT5X,
    ZOOM_3X,
    ZOOM_10X,
    ZOOM_100X,
    ZOOM_BASE
    };

void zoomRadioButtons(char *var, char *curVal)
/* Make a list of radio buttons for all zoom options */
{
int i;
int size = ArraySize(zoomOptions);
for (i = 0; i < size; i++)
    {
    char *s = zoomOptions[i];
    cgiMakeRadioButton(var, s, sameString(s, curVal));
    printf(" %s &nbsp;&nbsp;", s);
    }
}

/****** Some stuff for affy related controls *******/

static char *affyOptions[] = 
    {
    "Chip Type",
    "Chip ID",
    "Tissue Averages"
    };

enum affyOptEnum affyStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, affyOptions);
if (x < 0)
    errAbort("hui::affyStringToEnum() - Unknown option %s", string);
return x;
}

char *affyEnumToString(enum affyOptEnum x)
/* Convert from enum to string representation. */
{
return affyOptions[x];
}

void affyDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, affyOptions, ArraySize(affyOptions),
    curVal);
}

/****** Some stuff for affy all exon related controls *******/

static char *affyAllExonOptions[] = 
    {
    "Chip",
    "Tissue Averages"
    };

enum affyAllExonOptEnum affyAllExonStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, affyAllExonOptions);
if (x < 0)
    errAbort("hui::affyAllExonStringToEnum() - Unknown option %s", string);
return x;
}

char *affyAllExonEnumToString(enum affyAllExonOptEnum x)
/* Convert from enum to string representation. */
{
return affyAllExonOptions[x];
}

void affyAllExonDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, affyAllExonOptions, ArraySize(affyAllExonOptions),
    curVal);
}

/****** Some stuff for Rosetta related controls *******/

static char *rosettaOptions[] = 
    {
    "All Experiments",
    "Common Reference and Other",
    "Common Reference",
    "Other Exps"
    };

static char *rosettaExonOptions[] = 
    {
    "Confirmed Only",
    "Predicted Only",
    "All",
    };

enum rosettaOptEnum rosettaStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, rosettaOptions);
if (x < 0)
    errAbort("hui::rosettaStringToEnum() - Unknown option %s", string);
return x;
}

char *rosettaEnumToString(enum rosettaOptEnum x)
/* Convert from enum to string representation. */
{
return rosettaOptions[x];
}

void rosettaDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, rosettaOptions, ArraySize(rosettaOptions),
    curVal);
}

enum rosettaExonOptEnum rosettaStringToExonEnum(char *string)
/* Convert from string to enum representation of exon types. */
{
int x = stringIx(string, rosettaExonOptions);
if (x < 0)
    errAbort("hui::rosettaStringToExonEnum() - Unknown option %s", string);
return x;
}

char *rosettaExonEnumToString(enum rosettaExonOptEnum x)
/* Convert from enum to string representation of exon types. */
{
return rosettaExonOptions[x];
}

void rosettaExonDropDown(char *var, char *curVal)
/* Make drop down of exon type options. */
{
cgiMakeDropList(var, rosettaExonOptions, ArraySize(rosettaExonOptions), curVal);
}

/****** Options for the net track level display options *******/
static char *netLevelOptions[] = 
    {
    NET_LEVEL_0,
    NET_LEVEL_1,
    NET_LEVEL_2,
    NET_LEVEL_3,
    NET_LEVEL_4,
    NET_LEVEL_5,
    NET_LEVEL_6
    };

enum netLevelEnum netLevelStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, netLevelOptions);
if (x < 0)
    errAbort("hui::netLevelStringToEnum() - Unknown option %s", string);
return x;
}

char *netLevelEnumToString(enum netLevelEnum x)
/* Convert from enum to string representation. */
{
return netLevelOptions[x];
}

void netLevelDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, netLevelOptions, ArraySize(netLevelOptions), curVal);
}

/****** Options for the net track color options *******/
static char *netColorOptions[] = 
    {
    CHROM_COLORS,
    GRAY_SCALE
    };

enum netColorEnum netColorStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, netColorOptions);
if (x < 0)
    errAbort("hui::netColorStringToEnum() - Unknown option %s", string);
return x;
}

char *netColorEnumToString(enum netColorEnum x)
/* Convert from enum to string representation. */
{
return netColorOptions[x];
}

void netColorDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, netColorOptions, ArraySize(netColorOptions), curVal);
}

/****** Options for the chain track color options *******/
static char *chainColorOptions[] = 
    {
    CHROM_COLORS,
    SCORE_COLORS,
    NO_COLORS
    };

enum chainColorEnum chainColorStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, chainColorOptions);
if (x < 0)
    errAbort("hui::chainColorStringToEnum() - Unknown option %s", string);
return x;
}

char *chainColorEnumToString(enum chainColorEnum x)
/* Convert from enum to string representation. */
{
return chainColorOptions[x];
}

void chainColorDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, chainColorOptions, ArraySize(chainColorOptions), curVal);
}

/****** Options for the wiggle track Windowing *******/

static char *wiggleWindowingOptions[] = 
    {
    "mean+whiskers",
    "maximum",
    "mean",
    "minimum",
    "sum",
    };

enum wiggleWindowingEnum wiggleWindowingStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, wiggleWindowingOptions);
if (x < 0)
    errAbort("hui::wiggleWindowingStringToEnum() - Unknown option %s", string);
return x;
}

char *wiggleWindowingEnumToString(enum wiggleWindowingEnum x)
/* Convert from enum to string representation. */
{
return wiggleWindowingOptions[x];
}

void wiggleWindowingDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, wiggleWindowingOptions, ArraySize(wiggleWindowingOptions),
    curVal);
}

/****** Options for the wiggle track Smoothing *******/

static char *wiggleSmoothingOptions[] = 
    {
    "OFF", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11",
    "12", "13", "14", "15", "16"
    };

enum wiggleSmoothingEnum wiggleSmoothingStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, wiggleSmoothingOptions);
if (x < 0)
    errAbort("hui::wiggleSmoothingStringToEnum() - Unknown option %s", string);
return x;
}

char *wiggleSmoothingEnumToString(enum wiggleSmoothingEnum x)
/* Convert from enum to string representation. */
{
return wiggleSmoothingOptions[x];
}

void wiggleSmoothingDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, wiggleSmoothingOptions, ArraySize(wiggleSmoothingOptions),
    curVal);
}

/****** Options for the wiggle track y Line Mark On/Off *******/

static char *wiggleYLineMarkOptions[] = 
    {
    "OFF",
    "ON"
    };

enum wiggleYLineMarkEnum wiggleYLineMarkStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, wiggleYLineMarkOptions);
if (x < 0)
    errAbort("hui::wiggleYLineMarkStringToEnum() - Unknown option %s", string);
return x;
}

char *wiggleYLineMarkEnumToString(enum wiggleYLineMarkEnum x)
/* Convert from enum to string representation. */
{
return wiggleYLineMarkOptions[x];
}

void wiggleYLineMarkDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, wiggleYLineMarkOptions, ArraySize(wiggleYLineMarkOptions),
    curVal);
}

/****** Options for the wiggle track AutoScale *******/

static char *wiggleScaleOptionsParent[] = 
    {
    "use vertical viewing range setting",
    "auto-scale to data view",
    "group auto-scale"
    };

static char *wiggleScaleOptions[] = 
    {
    "use vertical viewing range setting",
    "auto-scale to data view"
    };

enum wiggleScaleOptEnum wiggleScaleStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, wiggleScaleOptionsParent);
if (x < 0)
    errAbort("hui::wiggleScaleStringToEnum() - Unknown option %s", string);
return x;
}

char *wiggleScaleEnumToString(enum wiggleScaleOptEnum x)
/* Convert from enum to string representation. */
{
return wiggleScaleOptionsParent[x];
}

void wiggleScaleDropDownParent(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, wiggleScaleOptionsParent, ArraySize(wiggleScaleOptionsParent),
    curVal);
}

void wiggleScaleDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, wiggleScaleOptions, ArraySize(wiggleScaleOptions),
    curVal);
}

/****** Options for the wiggle track type of graph *******/

static char *wiggleGraphOptions[] = 
    {
    "points",
    "bar",
    };

enum wiggleGraphOptEnum wiggleGraphStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, wiggleGraphOptions);
if (x < 0)
    errAbort("hui::wiggleGraphStringToEnum() - Unknown option %s", string);
return x;
}

char *wiggleGraphEnumToString(enum wiggleGraphOptEnum x)
/* Convert from enum to string representation. */
{
return wiggleGraphOptions[x];
}

void wiggleGraphDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, wiggleGraphOptions, ArraySize(wiggleGraphOptions), curVal);
}

static char *aggregateExtraLabels[] =
{
"none",
"transparent",
"solid",
"stacked",
"add",
"subtract",
};

static char *aggregateExtraValues[] =
{
WIG_AGGREGATE_NONE,
WIG_AGGREGATE_TRANSPARENT,
WIG_AGGREGATE_SOLID,
WIG_AGGREGATE_STACKED,
WIG_AGGREGATE_ADD,
WIG_AGGREGATE_SUBTRACT,
};

static char *aggregateLabels[] =
{
"none",
"transparent",
"solid",
"stacked",
};

static char *aggregateValues[] =
{
WIG_AGGREGATE_NONE,
WIG_AGGREGATE_TRANSPARENT,
WIG_AGGREGATE_SOLID,
WIG_AGGREGATE_STACKED,
};

char *wiggleAggregateFunctionEnumToString(enum wiggleAggregateFunctionEnum x)
/* Convert from enum to string representation. */
{
return aggregateValues[x];
}

enum wiggleAggregateFunctionEnum wiggleAggregateFunctionStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, aggregateExtraValues);
if (x < 0)
    errAbort("hui::wiggleAggregateFunctionStringToEnum() - Unknown option %s", string);
return x;
}

void aggregateExtraDropDown(char *var, char *curVal)
/* Make drop down menu for aggregate plus strategy */
{
cgiMakeDropListFull(var, aggregateExtraLabels, aggregateExtraValues,
    ArraySize(aggregateExtraValues), curVal, NULL, NULL);
}

void aggregateDropDown(char *var, char *curVal)
/* Make drop down menu for aggregate strategy */
{
cgiMakeDropListFull(var, aggregateLabels, aggregateValues,
    ArraySize(aggregateValues), curVal, NULL, NULL);
}

static char *viewFuncLabels[] =
{
"show all",
"add all",
"subtract from the first",
};

static char *viewFuncValues[] =
{
WIG_VIEWFUNC_SHOW_ALL,
WIG_VIEWFUNC_ADD_ALL,
WIG_VIEWFUNC_SUBTRACT_ALL,
};

char *wiggleViewFuncEnumToString(enum wiggleViewFuncEnum x)
/* Convert from enum to string representation. */
{
return viewFuncLabels[x];
}

enum wiggleViewFuncEnum wiggleViewFuncStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, viewFuncValues);
if (x < 0)
    errAbort("hui::wiggleViewFuncStringToEnum() - Unknown option %s", string);
return x;
}

void viewFuncDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropListFull(var, viewFuncLabels, viewFuncValues,
ArraySize(viewFuncValues), curVal, NULL, NULL);
}

static char *wiggleTransformFuncOptions[] = 
    {
    "NONE",
    "LOG"
    };

static char *wiggleTransformFuncLabels[] = 
    {
    "NONE",
    "LOG (ln(1+x))"
    };

enum wiggleTransformFuncEnum wiggleTransformFuncToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, wiggleTransformFuncOptions);
if (x < 0)
    errAbort("hui::wiggleTransformFuncToEnum() - Unknown option %s", string);
return x;
}

void wiggleTransformFuncDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropListFull(var, wiggleTransformFuncLabels, wiggleTransformFuncOptions,
ArraySize(wiggleTransformFuncOptions), curVal, NULL, NULL);
}

static char *wiggleAlwaysZeroOptions[] = 
    {
    "OFF",
    "ON"
    };

enum wiggleAlwaysZeroEnum wiggleAlwaysZeroToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, wiggleAlwaysZeroOptions);
if (x < 0)
    errAbort("hui::wiggleAlwaysZeroToEnum() - Unknown option %s", string);
return x;
}

void wiggleAlwaysZeroDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, wiggleAlwaysZeroOptions,
ArraySize(wiggleAlwaysZeroOptions), curVal);
}


/****** Options for the wiggle track horizontal grid lines *******/

static char *wiggleGridOptions[] = 
    {
    "ON",
    "OFF"
    };

enum wiggleGridOptEnum wiggleGridStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, wiggleGridOptions);
if (x < 0)
    errAbort("hui::wiggleGridStringToEnum() - Unknown option %s", string);
return x;
}

char *wiggleGridEnumToString(enum wiggleGridOptEnum x)
/* Convert from enum to string representation. */
{
return wiggleGridOptions[x];
}

void wiggleGridDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, wiggleGridOptions, ArraySize(wiggleGridOptions),
    curVal);
}

/****** Some stuff for wiggle track related controls *******/

static char *wiggleOptions[] = 
    {
    "samples only",
    "linear interpolation"
    };

enum wiggleOptEnum wiggleStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, wiggleOptions);
if (x < 0)
    errAbort("hui::wiggleStringToEnum() - Unknown option %s", string);
return x;
}

char *wiggleEnumToString(enum wiggleOptEnum x)
/* Convert from enum to string representation. */
{
return wiggleOptions[x];
}

void wiggleDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, wiggleOptions, ArraySize(wiggleOptions),
    curVal);
}


/****** Some stuff for GCwiggle track related controls *******/

static char *GCwiggleOptions[] = 
    {
    "samples only",
    "linear interpolation"
    };

enum GCwiggleOptEnum GCwiggleStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, GCwiggleOptions);
if (x < 0)
    errAbort("hui::GCwiggleStringToEnum() - Unknown option %s", string);
return x;
}

char *GCwiggleEnumToString(enum GCwiggleOptEnum x)
/* Convert from enum to string representation. */
{
return GCwiggleOptions[x];
}

void GCwiggleDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, GCwiggleOptions, ArraySize(GCwiggleOptions),
    curVal);
}

/****** Some stuff for chimp track related controls *******/

static char *chimpOptions[] = 
    {
    "samples only",
    "linear interpolation"
    };

enum chimpOptEnum chimpStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, chimpOptions);
if (x < 0)
    errAbort("hui::chimpStringToEnum() - Unknown option %s", string);
return x;
}

char *chimpEnumToString(enum chimpOptEnum x)
/* Convert from enum to string representation. */
{
return chimpOptions[x];
}

void chimpDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, chimpOptions, ArraySize(chimpOptions),
    curVal);
}


/*
#define POP_METHOD_AUTO "auto"
#define POP_METHOD_MANUAL "manual"

static char *popMethodLabels[] =
{
"auto",
"manual",
};

static char *popMethodValues[] =
{
POP_METHOD_AUTO,
POP_METHOD_MANUAL,
};
*/

/****** Some stuff for mRNA and EST related controls *******/

static void addMrnaFilter(struct mrnaUiData *mud, char *track, char *label, char *key, char *table)
/* Add an mrna filter */
{
struct mrnaFilter *fil;
AllocVar(fil);
fil->label = label;
fil->suffix = cloneString(key);
fil->table = table;
slAddTail(&mud->filterList, fil);
}

static struct mrnaUiData *newEmptyMrnaUiData(char *track)
/* Make a new  in extra-ui data structure for a bed. */
{
struct mrnaUiData *mud;
AllocVar(mud);
mud->filterTypeSuffix = cloneString("Ft");
mud->logicTypeSuffix = cloneString("Lt");
return mud;
}

struct mrnaUiData *newBedUiData(char *track)
/* Make a new  in extra-ui data structure for a bed. */
{
struct mrnaUiData *mud = newEmptyMrnaUiData(track);
addMrnaFilter(mud, track, "name", "name",track);
return mud;
}

struct mrnaUiData *newMrnaUiData(char *track, boolean isXeno)
/* Make a new  in extra-ui data structure for mRNA. */
{
struct mrnaUiData *mud = newEmptyMrnaUiData(track);
if (isXeno)
    addMrnaFilter(mud, track, "organism", "org", organismTable);
addMrnaFilter(mud, track, "accession", "acc", "acc");
addMrnaFilter(mud, track, "author", "aut", authorTable);
addMrnaFilter(mud, track, "library", "lib", libraryTable);
addMrnaFilter(mud, track, "tissue", "tis", tissueTable);
addMrnaFilter(mud, track, "cell", "cel", cellTable);
addMrnaFilter(mud, track, "keyword", "key", keywordTable);
addMrnaFilter(mud, track, "gene", "gen", geneNameTable);
addMrnaFilter(mud, track, "product", "pro", productNameTable);
addMrnaFilter(mud, track, "description", "des", descriptionTable);
return mud;
}

int trackNameAndLabelCmp(const void *va, const void *vb)
// Compare to sort on label.
{
const struct trackNameAndLabel *a = *((struct trackNameAndLabel **)va);
const struct trackNameAndLabel *b = *((struct trackNameAndLabel **)vb);
return strcmp(a->label, b->label);
}

char *trackFindLabel(struct trackNameAndLabel *list, char *label)
// Try to find label in list. Return NULL if it's not there.
{
struct trackNameAndLabel *el;
for (el = list; el != NULL; el = el->next)
    {
    if (sameString(el->label, label))
	return label;
    }
return NULL;
}

char *genePredDropDown(struct cart *cart, struct hash *trackHash,
				    char *formName, char *varName)
/* Make gene-prediction drop-down().  Return track name of
* currently selected one.  Return NULL if no gene tracks.
* If formName isn't NULL, it's the form for auto submit (onchange attr).
* If formName is NULL, no submit occurs when menu is changed */
{
char *cartTrack = cartOptionalString(cart, varName);
struct hashEl *trackList, *trackEl;
char *selectedName = NULL;
struct trackNameAndLabel *nameList = NULL, *name;
char *trackName = NULL;

/* Make alphabetized list of all genePred track names. */
trackList = hashElListHash(trackHash);
for (trackEl = trackList; trackEl != NULL; trackEl = trackEl->next)
    {
    struct trackDb *tdb = trackEl->val;
    char *dupe = cloneString(tdb->type);
    char *type = firstWordInLine(dupe);
    if ((sameString(type, "genePred")) && (!sameString(tdb->table, "tigrGeneIndex") && !tdbIsComposite(tdb) && !tdbIsCompositeView(tdb)))
	{
	AllocVar(name);
	name->name = tdb->track;
	name->label = tdb->longLabel;
	slAddHead(&nameList, name);
	}
    freez(&dupe);
    }
slSort(&nameList, trackNameAndLabelCmp);

/* No gene tracks - not much we can do. */
if (nameList == NULL)
    {
    slFreeList(&trackList);
    return NULL;
    }

/* Try to find current track - from cart first, then
* knownGenes, then refGenes. */
if (cartTrack != NULL)
    selectedName = trackFindLabel(nameList, cartTrack);
if (selectedName == NULL)
    selectedName = trackFindLabel(nameList, "Known Genes");
if (selectedName == NULL)
    selectedName = trackFindLabel(nameList, "SGD Genes");
if (selectedName == NULL)
    selectedName = trackFindLabel(nameList, "BDGP Genes");
if (selectedName == NULL)
    selectedName = trackFindLabel(nameList, "WormBase Genes");
if (selectedName == NULL)
    selectedName = trackFindLabel(nameList, "RefSeq Genes");
if (selectedName == NULL)
    selectedName = nameList->name;

/* Make drop-down list. */
    {
    char javascript[SMALLBUF], *autoSubmit, *event;
    int nameCount = slCount(nameList);
    char **menu;
    int i;

    AllocArray(menu, nameCount);
    for (name = nameList, i=0; name != NULL; name = name->next, ++i)
	{
	menu[i] = name->label;
	}
    if (formName == NULL)
	{
	autoSubmit = NULL;
	event = NULL;
	}
    else
	{
	safef(javascript, sizeof(javascript),
		"document.%s.submit();", formName);
	autoSubmit = javascript;
	event = "change"; 
	}
    cgiMakeDropListFull(varName, menu, menu, nameCount, selectedName, event, autoSubmit);
    freez(&menu);
    }

/* Convert to track name */
for (name = nameList; name != NULL; name = name->next)
    {
    if (sameString(selectedName, name->label))
	trackName = name->name;
    }

/* Clean up and return. */
slFreeList(&nameList);
slFreeList(&trackList);
return trackName;
}

void rAddTrackListToHash(struct hash *trackHash, struct trackDb *tdbList, char *chrom,
    boolean leafOnly)
/* Recursively add trackList to trackHash */
{
struct trackDb *tdb;
for (tdb = tdbList; tdb != NULL; tdb = tdb->next)
    {
    if (hTrackOnChrom(tdb, chrom))
	{
	if (tdb->subtracks == NULL || !leafOnly)
	    hashAdd(trackHash, tdb->track, tdb);
	}
    rAddTrackListToHash(trackHash, tdb->subtracks, chrom, leafOnly);
    }
}

struct hash *trackHashMakeWithComposites(char *db,char *chrom,struct trackDb **tdbList,
				     bool withComposites)
// Make hash of trackDb items for this chromosome, including composites, not just the subtracks.
// May pass in prepopulated trackDb list, or may receive the trackDb list as an inout.
{
struct trackDb *theTdbs = NULL;
if (tdbList == NULL || *tdbList == NULL)
    {
    theTdbs = hTrackDb(db);
    if (tdbList != NULL)
	*tdbList = theTdbs;
    }
else
    theTdbs = *tdbList;
struct hash *trackHash = newHash(7);
rAddTrackListToHash(trackHash, theTdbs, chrom, !withComposites);
return trackHash;
}

/****** Stuff for acembly related options *******/

static char *acemblyOptions[] = 
    {
    "all genes",
    "main",
    "putative",
    };

enum acemblyOptEnum acemblyStringToEnum(char *string)
/* Convert from string to enum representation. */
{
int x = stringIx(string, acemblyOptions);
if (x < 0)
    errAbort("hui::acemblyStringToEnum() - Unknown option %s", string);
return x;
}

char *acemblyEnumToString(enum acemblyOptEnum x)
/* Convert from enum to string representation. */
{
return acemblyOptions[x];
}

void acemblyDropDown(char *var, char *curVal)
/* Make drop down of options. */
{
cgiMakeDropList(var, acemblyOptions, ArraySize(acemblyOptions),
    curVal);
}

static boolean parseAssignment(char *words, char **name, char **value)
/* parse <name>=<value>, destroying input words in the process */
{
char *p;
if ((p = index(words, '=')) == NULL)
    return FALSE;
*p++ = 0;
if (name)
    *name = words;
if (value)
    *value = p;
return TRUE;
}

static char *getPrimaryType(char *primarySubtrack, struct trackDb *tdb)
/* Do not free when done. */
{
char *type = NULL;
if (primarySubtrack)
    {
    struct slRef *tdbRef, *tdbRefList = trackDbListGetRefsToDescendants(tdb->subtracks);
    for (tdbRef = tdbRefList; tdbRef != NULL; tdbRef = tdbRef->next)
	{
	struct trackDb *subtrack = tdbRef->val;
	if (sameString(subtrack->track, primarySubtrack))
	    {
	    type = subtrack->type;
	    break;
	    }
	}
    slFreeList(&tdbRefList);
    }
return type;
}

boolean hSameTrackDbType(char *type1, char *type2)
// Compare type strings: identical first word or allowed compatibilities
{
// OLD_CODE
//return (sameString(type1, type2) ||
//        (startsWith("wig ", type1) && startsWith("wig ", type2)));
int wordLength = strlen(type1);

// Many types have additional args which should not interfere (e.g. "bigWig 0 20")
// Note: beds of different size are ok
char *firstWhite = skipToSpaces(type1);
if (firstWhite != NULL)
    wordLength = (firstWhite - type1) + 1; // include white space

if (sameStringN(type1, type2,wordLength))
    return TRUE;

// Allow these cross overs?  Why not?  (see redmine #7588)
if (startsWith("wig ",type1) && startsWith("bigWig ",type2))  // tested
    return TRUE;
if (startsWith("bigWig ",type1) && startsWith("wig ",type2))  // tested
    return TRUE;

// Many flavors of bed that could be merged...
if ((   startsWith("bed ",type1)  // bed to Peak and vis-versa tested
 || startsWith("broadPeak",type1)
 || startsWith("narrowPeak",type1))
&&  (   startsWith("bed ",type2)
 || startsWith("broadPeak",type2)
 || startsWith("narrowPeak",type2)))
    return TRUE;
// bigBed to bed and vis-versa fails!
//if ((   startsWith("bed ",type1)
//     || startsWith("bigBed ",type1)
//     || startsWith("broadPeak",type1)
//     || startsWith("narrowPeak",type1))
//&&  (   startsWith("bed ",type2)
//     || startsWith("bigBed ",type2)
//     || startsWith("broadPeak",type2)
//     || startsWith("narrowPeak",type2)))
//    return TRUE;

return FALSE;
}

static char *labelRoot(char *label, char** suffix)
/* Parses a label which may be split with a &nbsp; into root and suffix
Always free labelRoot.  suffix, which may be null does not need to be freed. */
{
char *root = cloneString(label);
char *extra=strstrNoCase(root,"&nbsp;"); // &nbsp; mean don't include the reset as part of the link
if ((long)(extra)==-1)
    extra=NULL;
if (extra!=NULL)
    {
    *extra='\0';
    if (suffix != NULL)
	{
	extra+=5;
	*extra=' '; // Converts the &nbsp; to ' ' and include the ' '
	*suffix = extra;
	}
    }
return root;
}

typedef struct _dividers
    {
    int count;
    char**subgroups;
    char* setting;
    } dividers_t;

static dividers_t *dividersSettingGet(struct trackDb *parentTdb)
// Parses any dividers setting in parent of subtracks
{
dividers_t *dividers = needMem(sizeof(dividers_t));
dividers->setting    = cloneString(trackDbSetting(parentTdb, "dividers"));
if (dividers->setting == NULL)
    {
    freeMem(dividers);
    return NULL;
    }
dividers->subgroups  = needMem(24*sizeof(char*));
dividers->count      = chopByWhite(dividers->setting, dividers->subgroups,24);
return dividers;
}

static void dividersFree(dividers_t **dividers)
// frees any previously obtained dividers setting
{
if (dividers && *dividers)
    {
    freeMem((*dividers)->subgroups);
    freeMem((*dividers)->setting);
    freez(dividers);
    }
}

typedef struct _hierarchy
    {
    int count;
    char* subgroup;
    char**membership;
    int*  indents;
    char* setting;
    } hierarchy_t;

static hierarchy_t *hierarchySettingGet(struct trackDb *parentTdb)
// Parses any list hierachy instructions setting in parent of subtracks
{
hierarchy_t *hierarchy = needMem(sizeof(hierarchy_t));
hierarchy->setting     = cloneString(trackDbSetting(parentTdb, "hierarchy"));  // To be freed later
if (hierarchy->setting == NULL)
    {
    freeMem(hierarchy);
    return NULL;
    }
int cnt,ix;
char *words[SMALLBUF];
cnt = chopLine(hierarchy->setting, words);
assert(cnt<=ArraySize(words));
if (cnt <= 1)
    {
    freeMem(hierarchy->setting);
    freeMem(hierarchy);
    return NULL;
    }

hierarchy->membership  = needMem(cnt*sizeof(char*));
hierarchy->indents     = needMem(cnt*sizeof(int));
hierarchy->subgroup    = words[0];
char *name,*value;
for (ix = 1,hierarchy->count=0; ix < cnt; ix++)
    {
    if (parseAssignment(words[ix], &name, &value))
	{
	hierarchy->membership[hierarchy->count]  = name;
	hierarchy->indents[hierarchy->count] = sqlUnsigned(value);
	hierarchy->count++;
	}
    }
return hierarchy;
}

static void hierarchyFree(hierarchy_t **hierarchy)
// frees any previously obtained hierachy settings
{
if (hierarchy && *hierarchy)
    {
    freeMem((*hierarchy)->setting);
    freeMem((*hierarchy)->membership);
    freeMem((*hierarchy)->indents);
    freez(hierarchy);
    }
}

// Four State checkboxes can be checked/unchecked by enable/disabled
// NOTE: fourState is not a bitmap because it is manipulated in javascript and
//       int seemed easier at the time
#define FOUR_STATE_EMPTY             TDB_EXTRAS_EMPTY_STATE
//#define FOUR_STATE_UNCHECKED         0
//#define FOUR_STATE_CHECKED           1
//#define FOUR_STATE_CHECKED_DISABLED  -1
#define FOUR_STATE_DISABLE(val)      {while ((val) >= 0) (val) -= 2;}
#define FOUR_STATE_ENABLE(val)       {while ((val) < 0) (val) += 2;}

int subtrackFourStateChecked(struct trackDb *subtrack, struct cart *cart)
// Returns the four state checked state of the subtrack
{
char * setting = NULL;
char objName[SMALLBUF];
int fourState = (int)tdbExtrasFourState(subtrack);
if (fourState != FOUR_STATE_EMPTY)
    return fourState;

fourState = FOUR_STATE_UNCHECKED;  // default to unchecked, enabled
if ((setting = trackDbLocalSetting(subtrack, "parent")) != NULL)
    {
    if (findWordByDelimiter("off",' ',setting) == NULL)
	fourState = FOUR_STATE_CHECKED;
    }

// Now check visibility
enum trackVisibility vis = tdbLocalVisibility(cart, subtrack, NULL);
if (vis == tvHide)
    {
    if (tdbIsCompositeView(subtrack->parent))
	{
	if (tdbLocalVisibility(cart, subtrack->parent, NULL) == tvHide)
	    FOUR_STATE_DISABLE(fourState);
	}
    }

safef(objName, sizeof(objName), "%s_sel", subtrack->track);
setting = cartOptionalString(cart, objName);
if (setting == NULL)
    setting = cartOptionalString(cart, trackHubSkipHubName(objName));
if (setting != NULL)
    {
    if (sameWord("on",setting)) // ouch! cartUsualInt was interpreting "on" as 0, which was bad bug!
	fourState = 1;
    else
	fourState = atoi(setting);
    }
tdbExtrasFourStateSet(subtrack,fourState);
return fourState;
}

void subtrackFourStateCheckedSet(struct trackDb *subtrack, struct cart *cart,boolean checked,
			     boolean enabled)
// Sets the fourState Checked in the cart and updates cached state
{
int fourState = ( checked ? FOUR_STATE_CHECKED : FOUR_STATE_UNCHECKED );
if (!enabled)
    FOUR_STATE_DISABLE(fourState);

char objName[SMALLBUF];
char objVal[5];
safef(objName, sizeof(objName), "%s_sel", subtrack->track);
safef(objVal, sizeof(objVal), "%d", fourState);
cartSetString(cart, objName, objVal);
tdbExtrasFourStateSet(subtrack,fourState);
}


static char *tagEncode(char *name)
// Turns out css classes cannot begin with a number.  So prepend 'A'
// If this were more widely used, could move to cheapcgi.c.
{
if (!isdigit(*name))
    return name;

char *newName = needMem(strlen(name)+2);
*newName = 'A';
strcpy(newName+1,name);
return newName;
}


boolean dimensionsExist(struct trackDb *parentTdb)
// Does this parent track contain dimensions?
{
return (trackDbSetting(parentTdb, "dimensions") != NULL);
}

static dimensions_t *dimensionSettingsGet(struct trackDb *parentTdb)
// Parses any dimemnions setting in parent of subtracks
{
dimensions_t *dimensions = needMem(sizeof(dimensions_t));
dimensions->setting = cloneString(trackDbSetting(parentTdb, "dimensions"));  // To be freed later
if (dimensions->setting == NULL)
    {
    freeMem(dimensions);
    return NULL;
    }
int cnt,ix;
char *words[SMALLBUF];
cnt = chopLine(dimensions->setting,words);
assert(cnt<=ArraySize(words));
if (cnt <= 0)
    {
    freeMem(dimensions->setting);
    freeMem(dimensions);
    return NULL;
    }

dimensions->names     = needMem(cnt*sizeof(char*));
dimensions->subgroups = needMem(cnt*sizeof(char*));
char *name,*value;
for (ix = 0,dimensions->count=0; ix < cnt; ix++)
    {
    if (parseAssignment(words[ix], &name, &value))
	{
	dimensions->names[dimensions->count]     = name;
	dimensions->subgroups[dimensions->count] = tagEncode(value);
	dimensions->count++;
	}
    }
return dimensions;
}

static void dimensionsFree(dimensions_t **dimensions)
// frees any previously obtained dividers setting
{
if (dimensions && *dimensions)
    {
    freeMem((*dimensions)->setting);
    freeMem((*dimensions)->names);
    freeMem((*dimensions)->subgroups);
    freez(dimensions);
    }
}

#define SUBGROUP_MAX 9

int subgroupCount(struct trackDb *parentTdb)
// How many subGroup setting does this parent have?
{
int ix;
int count = 0;
for (ix=1;ix<=SUBGROUP_MAX;ix++)
    {
    char subGrp[16];
    safef(subGrp, ArraySize(subGrp), "subGroup%d",ix);
    if (trackDbSetting(parentTdb, subGrp) != NULL)
	count++;
    }
return count;
}

char * subgroupSettingByTagOrName(struct trackDb *parentTdb, char *groupNameOrTag)
// look for a subGroup by name (ie subGroup1) or tag (ie view) and return an unallocated char*
{
struct trackDb *ancestor;
for (ancestor = parentTdb; ancestor != NULL; ancestor = ancestor->parent)
    {
    char *setting = NULL;
    if (startsWith("subGroup",groupNameOrTag))
        {
        setting = trackDbSetting(ancestor, groupNameOrTag);
        if (setting != NULL)
            return setting;
        }
    // these views are cached at trackDb read time
    setting = trackDbViewSetting(ancestor, groupNameOrTag);
    if (setting != NULL)
        return setting;
    }
return NULL;
}

boolean subgroupingExists(struct trackDb *parentTdb, char *groupNameOrTag)
// Does this parent track contain a particular subgrouping?
{
return (subgroupSettingByTagOrName(parentTdb,groupNameOrTag) != NULL);
}

static members_t *subgroupMembersGet(struct trackDb *parentTdb, char *groupNameOrTag)
// Parse a subGroup setting line into tag,title, names(optional) and values(optional),
// returning the count of members or 0
{
static members_t nullMember;   // place holder for NULL
members_t *members  = tdbExtrasMembers(parentTdb, groupNameOrTag);
if (members != NULL)
    {
    if (members == &nullMember)
        return NULL;
    return members;
    }


int ix,count;
char *setting = subgroupSettingByTagOrName(parentTdb, groupNameOrTag);
if (setting == NULL)
    {
    tdbExtrasMembersSet(parentTdb, groupNameOrTag, &nullMember);
    return NULL;
    }
members = needMem(sizeof(members_t));
members->setting = cloneString(setting);
#define MAX_SUBGROUP_MEMBERS 2000
char *words[MAX_SUBGROUP_MEMBERS+3];    // members preceded by tag and title, one extra to detect
count = chopLine(members->setting, words);
if (count == ArraySize(words))
    warn("Subgroup %s exceeds maximum %d members", words[1], MAX_SUBGROUP_MEMBERS); 
if (count <= 1)
    {
    freeMem(members->setting);
    freeMem(members);
    tdbExtrasMembersSet(parentTdb, groupNameOrTag, &nullMember);
    return NULL;
    }
members->groupTag   = words[0];
members->groupTitle = strSwapChar(words[1],'_',' '); // Titles replace '_' with space
members->tags       = needMem(count*sizeof(char*));
members->titles     = needMem(count*sizeof(char*));
for (ix = 2,members->count=0; ix < count; ix++)
    {
    char *name,*value;
    if (parseAssignment(words[ix], &name, &value))
	{
	members->tags[members->count]  = tagEncode(name);
	members->titles[members->count] = strSwapChar(value,'_',' ');
	members->count++;
	}
    else
        {
        warn("Subgroup \"%s\" is missing a tag=val pair", words[1]);
        }
    }
tdbExtrasMembersSet(parentTdb, groupNameOrTag, members);

return members;
}

static int membersSubGroupIx(members_t* members, char *tag)
// Returns the index of the subgroup within the members struct (or -1)
{
int ix = 0;
for (ix=0;ix<members->count;ix++)
    {
    if (members->tags[ix] != NULL && sameString(members->tags[ix],tag))
	return ix;
    }
return -1;
}


static void subgroupMembersFree(members_t **members)
// frees memory for subgroupMembers lists
{
if (members && *members)
    {
    // This should only get set through membersForAll which should not be freed.
    if ((*members)->selected != NULL || (*members)->subtrackList != NULL)
	return;
    freeMem((*members)->setting);
    freeMem((*members)->tags);
    freeMem((*members)->titles);
    freez(members);
    }
}

static members_t *subgroupMembersWeedOutEmpties(struct trackDb *parentTdb, members_t *members,
					    struct cart *cart)
// Weed out members of a subgroup without any subtracks, alters memory in place!
{
if (members->count == 0)
    {
    warn("%s: No subtracks in group: %s.  This indicates a problem in the subGroup line for this group.",
         parentTdb->track, members->groupTitle);
    return members;
    }
// First tally all subtrack counts
int ixIn=0;
struct slRef *subtrackRef, *subtrackRefList =
				    trackDbListGetRefsToDescendantLeaves(parentTdb->subtracks);
struct trackDb *subtrack;
members->subtrackCount    = needMem(members->count * sizeof(int));
members->currentlyVisible = needMem(members->count * sizeof(int));
members->subtrackList     = needMem(members->count * sizeof(struct slRef *));
for (subtrackRef = subtrackRefList; subtrackRef != NULL; subtrackRef = subtrackRef->next)
    {
    subtrack = subtrackRef->val;
    char *belongsTo =NULL;
    if (subgroupFind(subtrack,members->groupTag,&belongsTo))
	{
	if (-1 != (ixIn = stringArrayIx(belongsTo, members->tags, members->count)))
	    {
	    members->subtrackCount[ixIn]++;
	    if (cart && fourStateVisible(subtrackFourStateChecked(subtrack,cart)))
		members->currentlyVisible[ixIn]++;
	    refAdd(&(members->subtrackList[ixIn]), subtrack);
	    }
	}
    }

// Now weed out empty subgroup tags.  Can do this in place since new count <= old count
// NOTE: Don't I wish I had made these as an slList ages ago! (tim)
int ixOut=0;
for (ixIn=ixOut;ixIn<members->count;ixIn++)
    {
    if (members->subtrackCount[ixIn] > 0)
	{
	if (ixOut < ixIn)
	    {
	    members->tags[ixOut]             = members->tags[ixIn];
	    members->titles[ixOut]           = members->titles[ixIn];
	    members->subtrackCount[ixOut]    = members->subtrackCount[ixIn];
	    members->currentlyVisible[ixOut] = members->currentlyVisible[ixIn];
	    members->subtrackList[ixOut]     = members->subtrackList[ixIn];
	    if (members->selected != NULL)
		members->selected[ixOut]     = members->selected[ixIn];
	    }
	ixOut++;
	}
    else
	{
	members->tags[ixIn]             = NULL;
	members->titles[ixIn]           = NULL;
	members->subtrackCount[ixIn]    = 0;
	members->currentlyVisible[ixIn] = 0;
	//slFreeList(&members->subtrackList[ixIn]);  // No freeing at this moment
	members->subtrackList[ixIn]     = NULL;
	if (members->selected != NULL)
	    members->selected[ixIn]     = FALSE;
	}
    }
members->count = ixOut;

if (members->count == 0) // No members of this subgroup had a subtrack
    {
    //subgroupMembersFree(&members);   // don't bother freeing
    return NULL;
    }

return members;
}

enum
    {
    dimV=0, // View first
    dimX=1, // X & Y next
    dimY=2,
    dimA=3, // dimA is start of first of the optional non-matrix, non-view dimensions
    };



static char* abcMembersChecked(struct trackDb *parentTdb, struct cart *cart, members_t* members,
			   char letter)
// returns a string of subGroup tags which are currently checked
{
char settingName[SMALLBUF];
int mIx;
if (members->selected == NULL)
    members->selected = needMem(members->count * sizeof(boolean));
safef(settingName, sizeof(settingName), "%s.filterComp.%s",parentTdb->track,members->groupTag);
struct slName *options = cartOptionalSlNameList(cart,settingName);
if (options != NULL)
    {
    if (sameWord(options->name,"All")) // filterComp returns "All" meaning every option selected
	{
	slNameFreeList(&options);
	options = slNameListFromStringArray(members->tags, members->count);
	assert(options != NULL);
	}
    struct slName *option;
    for (option=options;option!=NULL;option=option->next)
	{
	mIx = membersSubGroupIx(members, option->name);
	if (mIx >= 0)
	    members->selected[mIx] = TRUE;
	}
    return slNameListToString(options,',');
    }
struct dyString *currentlyCheckedTags = NULL;
// Need a string of subGroup tags which are currently checked
safef(settingName,sizeof(settingName),"dimension%cchecked",letter);
char *dimCheckedDefaults = trackDbSettingOrDefault(parentTdb,settingName,"All");
char *checkedDefaults[12];
int defaultCount = 0;
if (dimCheckedDefaults != NULL
&& differentWord(dimCheckedDefaults,"All") && differentWord(dimCheckedDefaults,"Any"))
    {
    defaultCount = chopCommas(dimCheckedDefaults, checkedDefaults);
    int dIx = 0;
    for (;dIx < defaultCount;dIx++)
	checkedDefaults[dIx] = tagEncode(checkedDefaults[dIx]); // encode these before compare!
    }                                                      // Will leak, but this is a tiny amount
for (mIx=0;mIx<members->count;mIx++)
    {
    safef(settingName, sizeof(settingName), "%s.mat_%s_dim%c_cb",
	  parentTdb->track,members->tags[mIx],letter);
    members->selected[mIx] = TRUE;
    if (defaultCount > 0)
	members->selected[mIx] =
		(-1 != stringArrayIx(members->tags[mIx],checkedDefaults,defaultCount));
    members->selected[mIx] = cartUsualBoolean(cart,settingName,members->selected[mIx]);
    if (members->selected[mIx])
	{
	if (currentlyCheckedTags == NULL)
	    currentlyCheckedTags = dyStringCreate("%s",members->tags[mIx]);
	else
	    dyStringPrintf(currentlyCheckedTags,",%s",members->tags[mIx]);
	}
    }
if (currentlyCheckedTags)
    return dyStringCannibalize(&currentlyCheckedTags);
return NULL;
}

static membersForAll_t *membersForAllSubGroupsWeedOutEmpties(struct trackDb *parentTdb,
					    membersForAll_t *membersForAll, struct cart *cart)
// Weed through members, tossing those without subtracks
{
// View is always first
if (membersForAll->members[dimV] != NULL)
    membersForAll->members[dimV] =
		subgroupMembersWeedOutEmpties(parentTdb, membersForAll->members[dimV],cart);

// X and Y are special
if (membersForAll->members[dimX] != NULL)
    membersForAll->members[dimX] =
		subgroupMembersWeedOutEmpties(parentTdb, membersForAll->members[dimX],cart);
if (membersForAll->members[dimY] != NULL)
    membersForAll->members[dimY] =
		subgroupMembersWeedOutEmpties(parentTdb, membersForAll->members[dimY],cart);

// Handle the ABC dimensions
int ixIn,ixOut=dimA;
for (ixIn=ixOut;ixIn<membersForAll->dimMax;ixIn++)
    {
    if (membersForAll->members[ixIn] != NULL)
	membersForAll->members[ixIn] =
		    subgroupMembersWeedOutEmpties(parentTdb, membersForAll->members[ixIn],cart);
    if (membersForAll->members[ixIn] == NULL)
	membersForAll->checkedTags[ixOut] = NULL;
    else
	{
	if (ixOut < ixIn)  // Collapse if necessary
	    { // NOTE: Don't I wish I had made these as an slList ages ago! (tim)
	    membersForAll->members[ixOut]     = membersForAll->members[ixIn];
	    membersForAll->checkedTags[ixOut] = membersForAll->checkedTags[ixIn];
	    membersForAll->letters[ixOut]     = membersForAll->letters[ixIn];
	    }
	ixOut++;
	}
    }
membersForAll->dimMax   = ixOut;
membersForAll->abcCount = membersForAll->dimMax - dimA;

return membersForAll;
}

membersForAll_t* membersForAllSubGroupsGet(struct trackDb *parentTdb, struct cart *cart)
// Returns all the parents subGroups and members
{
membersForAll_t *membersForAll = tdbExtrasMembersForAll(parentTdb);
if (membersForAll != NULL)
    return membersForAll;  // Already retrieved, so don't do it again

int ix;
membersForAll = needMem(sizeof(membersForAll_t));
if (tdbIsCompositeView(parentTdb->subtracks))  // view must have viewInMidle tdb in tree
    membersForAll->members[dimV]=subgroupMembersGet(parentTdb,"view");
membersForAll->letters[dimV]='V';
membersForAll->dimMax=dimA;  // This can expand, depending upon ABC dimensions
membersForAll->dimensions = dimensionSettingsGet(parentTdb);
if (membersForAll->dimensions != NULL)
    {
    for (ix=0;ix<membersForAll->dimensions->count;ix++)
	{
	char letter = lastChar(membersForAll->dimensions->names[ix]);
	if (letter != 'X' && letter != 'Y')
	    {
	    membersForAll->members[membersForAll->dimMax] =
			subgroupMembersGet(parentTdb, membersForAll->dimensions->subgroups[ix]);
	    membersForAll->letters[membersForAll->dimMax] = letter;
	    if (cart != NULL)
		membersForAll->checkedTags[membersForAll->dimMax] = abcMembersChecked(
			    parentTdb,cart,membersForAll->members[membersForAll->dimMax],letter);
	    membersForAll->dimMax++;
	    }
	else if (letter == 'X')
	    {
	    membersForAll->members[dimX] =
			subgroupMembersGet(parentTdb, membersForAll->dimensions->subgroups[ix]);
	    membersForAll->letters[dimX] = letter;
	    }
	else
	    {
	    membersForAll->members[dimY] =
			subgroupMembersGet(parentTdb, membersForAll->dimensions->subgroups[ix]);
	    membersForAll->letters[dimY] = letter;
	    }
	}
    }
else // No 'dimensions" setting: treat any subGroups as abc dimensions
    {
    char letter = 'A';
    // walk through numbered subgroups
    for (ix=1;ix<SUBGROUP_MAX;ix++)  // how many do we support?
	{
	char group[32];
	safef(group, sizeof group,"subGroup%d",ix);
	char *setting = subgroupSettingByTagOrName(parentTdb, group);
	if (setting != NULL)
	    {
	    char *tag = cloneFirstWord(setting);
	    if (membersForAll->members[dimV] && sameWord(tag,"view"))
		continue; // View should have already been handled. NOTE: extremely unlikely case
	    membersForAll->members[membersForAll->dimMax]=subgroupMembersGet(parentTdb, tag);
	    membersForAll->letters[membersForAll->dimMax]=letter;
	    if (cart != NULL)
		membersForAll->checkedTags[membersForAll->dimMax] = abcMembersChecked(
			    parentTdb,cart,membersForAll->members[membersForAll->dimMax],letter);
	    membersForAll->dimMax++;
	    letter++;
	    }
	}
    }
membersForAll->abcCount = membersForAll->dimMax - dimA;

membersForAll = membersForAllSubGroupsWeedOutEmpties(parentTdb, membersForAll, cart);

// NOTE: Dimensions must be defined for filterComposite.  Filter dimensioms are all and only ABCs.
//       Use dimensionAchecked to define selected
char *filtering = trackDbSettingOrDefault(parentTdb,"filterComposite",NULL);
if (filtering && !sameWord(filtering,"off"))
    {
    if (membersForAll->dimensions == NULL)
	errAbort("If 'filterComposite' defined, must define 'dimensions' also.");

    membersForAll->filters = TRUE;
    // Default all to multi
    for (ix=dimA;ix<membersForAll->dimMax;ix++)
	{
	if (membersForAll->members[ix] != NULL)
	    membersForAll->members[ix]->fcType = fctMulti;
	}
    if (!sameWord(filtering,"on"))
	{
	// Example tdb setting: "filterComposite on" OR
	//                      "filterComposite dimA=one dimB=multi dimC=onlyOne"
	// FIXME: do we even support anything but multi???
	char *filterGroups[27];
	int count = chopLine(filtering,filterGroups);
	for (ix=0;ix<count;ix++)
	    {
	    char *dim = cloneNextWordByDelimiter(&filterGroups[ix],'=');
	    char letter = lastChar(dim);
	    int abcIx = dimA;
	    for (;abcIx < membersForAll->dimMax && membersForAll->letters[abcIx] != letter;abcIx++)
		; // Advance to correct letter
	    if (abcIx >= membersForAll->dimMax)
		errAbort("Invalid 'filterComposite' trackDb setting. Dimension '%s' not found.",
			 dim);
	    if (sameWord(filterGroups[ix],"one"))
		membersForAll->members[abcIx]->fcType = fctOne;
	    else if (sameWord(filterGroups[ix],"onlyOne") || sameWord(filterGroups[ix],"oneOnly"))
		membersForAll->members[abcIx]->fcType = fctOneOnly;
	    }
	}
    }

if (cart != NULL) // Only save this if it is fully populated!
    tdbExtrasMembersForAllSet(parentTdb,membersForAll);

return membersForAll;
}

static int membersForAllFindSubGroupIx(membersForAll_t* membersForAll, char *tag)
{ // Returns the index of the subgroups member struct within membersForAll (or -1)
int ix = 0;
for (ix=0;ix<membersForAll->dimMax;ix++)
    {
    if (membersForAll->members[ix] != NULL && sameString(membersForAll->members[ix]->groupTag,tag))
	return ix;
    }
return -1;
}

const members_t*membersFindByTag(struct trackDb *parentTdb, char *tag)
{ // Uses membersForAll which may be in tdbExtraCache.  Do not free
membersForAll_t* membersForAll = membersForAllSubGroupsGet(parentTdb,NULL);
if (membersForAll == NULL)
    return NULL;

int ix = membersForAllFindSubGroupIx(membersForAll,tag);
if (ix >= 0)
    return membersForAll->members[ix];
return NULL;
}

static void membersForAllSubGroupsFree(struct trackDb *parentTdb,
				   membersForAll_t** membersForAllPtr)
// frees memory for membersForAllSubGroups struct
{
return;    // don't bother freeing things, just takes time for no benefit

if (membersForAllPtr && *membersForAllPtr)
    {
    if (parentTdb != NULL)
	{
	if (*membersForAllPtr == tdbExtrasMembersForAll(parentTdb))
	    return;  // Don't free something saved to the tdbExtras!
	}
    membersForAll_t* membersForAll = *membersForAllPtr;
    subgroupMembersFree(&(membersForAll->members[dimX]));
    subgroupMembersFree(&(membersForAll->members[dimY]));
    subgroupMembersFree(&(membersForAll->members[dimV]));
    int ix;
    for (ix=dimA;ix<membersForAll->dimMax;ix++)
	{
	//ASSERT(membersForAll->members[ix] != NULL);
	subgroupMembersFree(&(membersForAll->members[ix]));
	if (membersForAll->checkedTags[ix])
	    freeMem(membersForAll->checkedTags[ix]);
	}
    dimensionsFree(&(membersForAll->dimensions));
    freez(membersForAllPtr);
    }
}

int multViewCount(struct trackDb *parentTdb)
// returns the number of multiView views declared
{
char *setting = subgroupSettingByTagOrName(parentTdb,"view");
if (setting == NULL)
    return 0;

setting = cloneString(setting);
int cnt;
char *words[32];
cnt = chopLine(setting, words);
freeMem(setting);
return (cnt - 1);
}


membership_t *subgroupMembershipGet(struct trackDb *childTdb)
// gets all the subgroup membership for a child track
{
membership_t *membership = tdbExtrasMembership(childTdb);
if (membership != NULL)
    return membership;  // Already retrieved, so don't do it again

membership = needMem(sizeof(membership_t));
membership->setting = cloneString(trackDbSetting(childTdb, "subGroups"));
if (membership->setting == NULL)
    {
    freeMem(membership);
    return NULL;
    }

int ix,cnt;
char *words[SMALLBUF];
cnt = chopLine(membership->setting, words);
assert(cnt < ArraySize(words));
if (cnt <= 0)
    {
    freeMem(membership->setting);
    freeMem(membership);
    return NULL;
    }
membership->subgroups  = needMem(cnt*sizeof(char*));
membership->membership = needMem(cnt*sizeof(char*));
membership->titles     = needMem(cnt*sizeof(char*));
for (ix = 0,membership->count=0; ix < cnt; ix++)
    {
    char *name,*value;
    if (parseAssignment(words[ix], &name, &value))
	{
	membership->subgroups[membership->count]  = name;
	membership->membership[membership->count] = tagEncode(value);
						    // tags will be used as classes by js
	members_t* members = subgroupMembersGet(childTdb->parent, name);
	membership->titles[membership->count] = NULL; // default
	if (members != NULL)
	    {
	    int ix2 = stringArrayIx(membership->membership[membership->count],members->tags,
				    members->count);
	    if (ix2 != -1)
		membership->titles[membership->count] =
					strSwapChar(cloneString(members->titles[ix2]),'_',' ');
//	    subgroupMembersFree(&members);   /// don't bother freeing
	    }
	membership->count++;
	}
    }
tdbExtrasMembershipSet(childTdb,membership);
return membership;
}


static boolean membershipInAllCurrentABCs(membership_t *membership,membersForAll_t*membersForAll)
// looks for a match between a membership set and ABC dimensions currently checked
{
int mIx,aIx,tIx;
for (aIx = dimA; aIx < membersForAll->dimMax; aIx++)  // for each ABC subGroup
    {
    assert(membersForAll->members[aIx]->selected);

    // must find atleast one selected tag that we have membership in
    boolean matched = FALSE;
    for (mIx = 0; mIx <membersForAll->members[aIx]->count;mIx++) // for each tag of that subgroup
	{
	if (membersForAll->members[aIx]->selected[mIx])  // The particular subgroup tag is selected
	    {
	    for (tIx=0;tIx<membership->count;tIx++)  // what we are members of
		{
		// subTrack belongs to subGroup and tags match
		if (sameString(membersForAll->members[aIx]->groupTag, membership->subgroups[tIx])
		&&   sameWord(membersForAll->members[aIx]->tags[mIx],membership->membership[tIx]))
		    {
		    matched = TRUE;
		    break;
		    }
		}
	    }
	}
    if (!matched)
	return FALSE;
    }
return TRUE; // passed all tests so must be on all
}

char *compositeGroupLabel(struct trackDb *tdb, char *group, char *id)
// Given ID from group, return corresponding label,  looking through parent's subGroupN's
// Given group ID, return group label,  looking through parent's subGroupN's
{
members_t *members = subgroupMembersGet(tdb, group);
char *result = NULL;
int i;
for (i=0; i<members->count; ++i)
    {
    if (sameString(members->tags[i], id))
	result = cloneString(members->titles[i]);
    }
//subgroupMembersFree(&members);   /// don't bother freeing
return result;
}

char *compositeGroupId(struct trackDb *tdb, char *group, char *label)
// Given label, return id,  looking through parent's subGroupN's
{
members_t *members = subgroupMembersGet(tdb, group);
char *result = NULL;
int i;
for (i=0; i<members->count; ++i)
    {
    if (sameString(members->titles[i], label))
	result = cloneString(members->tags[i]);
    }
//subgroupMembersFree(&members);   /// don't bother freeing
return result;
}


static boolean subtrackInAllCurrentABCs(struct trackDb *childTdb,membersForAll_t*membersForAll)
// looks for a match between a membership set and ABC dimensions currently checked
{
membership_t *membership = subgroupMembershipGet(childTdb);
if (membership == NULL)
    return FALSE;
boolean found = membershipInAllCurrentABCs(membership,membersForAll);
return found;
}


boolean subgroupFind(struct trackDb *childTdb, char *name,char **value)
// looks for a single tag in a child track's subGroups setting
{
if (value != NULL)
    *value = NULL;
membership_t *membership = subgroupMembershipGet(childTdb);
if (membership != NULL)
    {
    int ix;
    for (ix=0;ix<membership->count;ix++)
	{
	if (sameString(name,membership->subgroups[ix]))
	    {
	    if (value != NULL)
		*value = cloneString(membership->membership[ix]);
	    return TRUE;
	    }
	}
    }
return FALSE;
}

static char *subtrackColorToCompare(struct trackDb *subtrack)
/* Convert RGB color to string with scaled hue, suitable for alpha sort */
{
struct rgbColor rgbColor;
rgbColor.r = subtrack->colorR;
rgbColor.g = subtrack->colorG;
rgbColor.b = subtrack->colorB;
rgbColor.a = 255;
struct hslColor hslColor = mgRgbToHsl(rgbColor);
int hue = hslColor.h * 10;
char buf[5];
safef(buf, 5, "%04d", hue);
return cloneString(buf);
}

boolean subgroupFindTitle(struct trackDb *parentTdb, char *name,char **value)
// looks for a a subgroup matching the name and returns the title if found
{
if (value != (void*)NULL)
    *value = NULL;
members_t*members=subgroupMembersGet(parentTdb, name);
//const members_t *members = membersFindByTag(parentTdb,name);
//                           Can't use because of dimension dependence
if (members==NULL)
    return FALSE;
*value = cloneString(members->groupTitle);
//subgroupMembersFree(&members);
return TRUE;
}

void subgroupFree(char **value)
// frees subgroup memory
{
if (value && *value)
    freez(value);
}

boolean subgroupRequired(char *value)
/* Returns whether subgroup much be specified for each track.
* Generally true.  Exceptions are specially defined subgroups */
{
return differentString(SUBTRACK_COLOR_SUBGROUP, value);
}

#define SORT_ON_TRACK_NAME "trackName"
#define SORT_ON_RESTRICTED "dateUnrestricted"

sortOrder_t *sortOrderGet(struct cart *cart,struct trackDb *parentTdb)
// Parses any list sort order instructions for parent of subtracks (from cart or trackDb)
// Some trickiness here.  sortOrder->sortOrder is from cart (changed by user action),
// as is sortOrder->order, But columns are in original tdb order (unchanging)!
// However, if cart is null, all is from trackDb.ra
{
int ix;
char *setting = trackDbSetting(parentTdb, "sortOrder");
if (setting == NULL) // Must be in trackDb or not a sortable table
    return NULL;

sortOrder_t *sortOrder = needMem(sizeof(sortOrder_t));
sortOrder->htmlId = needMem(strlen(parentTdb->track)+15);
safef(sortOrder->htmlId, (strlen(parentTdb->track)+15), "%s.sortOrder", parentTdb->track);
char *cartSetting = NULL;
if (cart != NULL)
    cartSetting = cartCgiUsualString(cart, sortOrder->htmlId, setting);
// If setting is bigger than cartSetting, then it may be due to a trackDb change
if (cart != NULL && strlen(cartSetting) > strlen(setting))
    sortOrder->sortOrder = cloneString(cartSetting);  // cart order
else
    sortOrder->sortOrder = cloneString(setting);      // old cart value is abandoned!

/* parse setting into sortOrder */
sortOrder->setting = cloneString(setting);
sortOrder->count   = chopByWhite(sortOrder->setting,NULL,0);  // Get size
if (cart && !stringIn(SORT_ON_TRACK_NAME,setting))
    sortOrder->count += 1;
if (cart && !stringIn(SORT_ON_RESTRICTED,setting))
    sortOrder->count += 1;
sortOrder->column  = needMem(sortOrder->count*sizeof(char*));
int foundColumns = chopByWhite(sortOrder->setting, sortOrder->column,sortOrder->count);
sortOrder->title   = needMem(sortOrder->count*sizeof(char*));
sortOrder->forward = needMem(sortOrder->count*sizeof(boolean));
sortOrder->order   = needMem(sortOrder->count*sizeof(int));
if (cart && foundColumns < sortOrder->count)
    {
    int columnCount = foundColumns;
    int size = 0;
    char *moreOrder = NULL;
    if (cart && columnCount < sortOrder->count && !stringIn(SORT_ON_TRACK_NAME,setting))
	{
	assert(sortOrder->column[columnCount] == NULL);
	sortOrder->column[columnCount] = cloneString(SORT_ON_TRACK_NAME "=+");
	if (!stringIn(SORT_ON_TRACK_NAME,sortOrder->sortOrder))
	    {                                                                    // little bit more
	    size = strlen(sortOrder->sortOrder) + strlen(sortOrder->column[columnCount]) + 5;
	    moreOrder = needMem(size);
	    safef(moreOrder,size,"%s %s",sortOrder->sortOrder, sortOrder->column[columnCount]);
	    freeMem(sortOrder->sortOrder);
	    sortOrder->sortOrder = moreOrder;
	    }
	columnCount++;
	}
    if (cart && columnCount < sortOrder->count && !stringIn(SORT_ON_RESTRICTED,setting))
	{
	assert(sortOrder->column[columnCount] == NULL);
	sortOrder->column[columnCount] = cloneString(SORT_ON_RESTRICTED "=+");
	if (!stringIn(SORT_ON_RESTRICTED,sortOrder->sortOrder))
	    {
	    size = strlen(sortOrder->sortOrder) + strlen(sortOrder->column[columnCount]) + 5;
	    moreOrder = needMem(size);
	    safef(moreOrder,size,"%s %s",sortOrder->sortOrder, sortOrder->column[columnCount]);
	    freeMem(sortOrder->sortOrder);
	    sortOrder->sortOrder = moreOrder;
	    }
	columnCount++;
	}
    }
for (ix = 0; ix<sortOrder->count; ix++)
    {
    strSwapChar(sortOrder->column[ix],'=',0);  // Don't want 'CEL=+' but 'CEL' and '+'
    // find tdb substr in cart current order string
    char *pos = stringIn(sortOrder->column[ix], sortOrder->sortOrder);
    //assert(pos != NULL && pos[strlen(sortOrder->column[ix])] == '=');
    if (pos != NULL && pos[strlen(sortOrder->column[ix])] == '=')
	{
	int ord=1;
	char* pos2 = sortOrder->sortOrder;
	for (;*pos2 && pos2 < pos;pos2++)
	    {
	    if (*pos2 == '=') // Discovering sort order in cart
		ord++;
	    }
	sortOrder->forward[ix] = (pos[strlen(sortOrder->column[ix]) + 1] == '+');
	sortOrder->order[ix] = ord;
	}
    else  // give up on cartSetting
	{
	sortOrder->forward[ix] = TRUE;
	sortOrder->order[ix] = ix+1;
	}
    if (ix < foundColumns)
	{
	subgroupFindTitle(parentTdb,sortOrder->column[ix],&(sortOrder->title[ix]));
	}
    }
return sortOrder;  // NOTE cloneString:words[0]==*sortOrder->column[0]
}                  // and will be freed when sortOrder is freed

void sortOrderFree(sortOrder_t **sortOrder)
// frees any previously obtained sortOrder settings
{
if (sortOrder && *sortOrder)
    {
    int ix;
    for (ix=0;ix<(*sortOrder)->count;ix++) { subgroupFree(&((*sortOrder)->title[ix])); }
    freeMem((*sortOrder)->sortOrder);
    freeMem((*sortOrder)->htmlId);
    freeMem((*sortOrder)->column);
    freeMem((*sortOrder)->forward);
    freeMem((*sortOrder)->order);
    freeMem((*sortOrder)->setting);
    freez(sortOrder);
    }
}




sortableTdbItem *sortableTdbItemCreate(struct trackDb *tdbChild,sortOrder_t *sortOrder)
// creates a sortable tdb item struct, given a child tdb and its parent's sort table
// Errors in interpreting a passed in sortOrder will return NULL
{
sortableTdbItem *item = NULL;
if (tdbChild == NULL || tdbChild->shortLabel == NULL)
    return NULL;
AllocVar(item);
item->tdb = tdbChild;
if (sortOrder != NULL)   // Add some sort buttons
    {
    int sIx=0;
    for (sIx=sortOrder->count - 1;sIx>=0;sIx--) // walk backwards to ensure sort order in columns
	{
	sortColumn *column = NULL;
	AllocVar(column);
	column->fwd = sortOrder->forward[sIx];
	char *col = sortOrder->column[sIx];
	if (!subgroupFind(item->tdb, col, &(column->value)))
	    {
	    if (sameString(col, SUBTRACK_COLOR_SUBGROUP))
		{
		// convert RGB color to hue so alpha sort can compare
		column->value = subtrackColorToCompare(tdbChild);
		}
	    else
		{
		char *setting = trackDbSetting(item->tdb,col);
		if (setting != NULL)
		    column->value = cloneString(setting);
		// No subgroup, assume there is a matching setting (eg longLabel)
		}
	    }
	if (column->value != NULL)
	    slAddHead(&(item->columns), column);
	else
	    {
	    freez(&column);
	    if (item->columns != NULL)
		slFreeList(&(item->columns));
	    freeMem(item);
	    return NULL; // sortOrder setting doesn't match items to be sorted.
	    }
	}
    }
return item;
}

static int sortableTdbItemsCmp(const void *va, const void *vb)
// Compare two sortable tdb items based upon sort columns.
{
const sortableTdbItem *a = *((sortableTdbItem **)va);
const sortableTdbItem *b = *((sortableTdbItem **)vb);
sortColumn *colA = a->columns;
sortColumn *colB = b->columns;
int compared = 0;
for (;compared==0 && colA!=NULL && colB!=NULL;colA=colA->next,colB=colB->next)
    {
    if (colA->value != NULL && colB->value != NULL)
	compared = strcmp(colA->value, colB->value) * (colA->fwd? 1: -1);
    }
if (compared != 0)
    return compared;

return strcasecmp(a->tdb->shortLabel, b->tdb->shortLabel); // Last chance
}

void sortTdbItemsAndUpdatePriorities(sortableTdbItem **items)
// sort items in list and then update priorities of item tdbs
{
if (items != NULL && *items != NULL)
    {
    slSort(items, sortableTdbItemsCmp);
    int priority=1;
    sortableTdbItem *item;
    for (item = *items; item != NULL; item = item->next)
	item->tdb->priority = (float)priority++;
    }
}

void sortableTdbItemsFree(sortableTdbItem **items)
// Frees all memory associated with a list of sortable tdb items
{
if (items != NULL && *items != NULL)
    {
    sortableTdbItem *item;
    for (item = *items; item != NULL; item = item->next)
	{
	sortColumn *column;
	for (column = item->columns; column != NULL; column = column->next)
	    freeMem(column->value);
	slFreeList(&(item->columns));
	}
    slFreeList(items);
    }
}

static boolean colonPairToStrings(char * colonPair,char **first,char **second)
// Will set *first and *second to NULL.  Must free any string returned!
// No colon: value goes to *first
{
if (first)
    *first =NULL; // default to NULL !
if (second)
    *second=NULL;
if (colonPair != NULL)
    {
    if (strchr(colonPair,':'))
	{
	if (second)
	    *second = cloneString(strchr(colonPair,':') + 1);
	if (first)
	    *first = strSwapChar(cloneString(colonPair),':',0);
	}
    else if (first)
	*first = cloneString(colonPair);
    return (*first != NULL || *second != NULL);
    }
return FALSE;
}

boolean colonPairToInts(char * colonPair,int *first,int *second)
{ // Non-destructive. Only sets values if found. No colon: value goes to *first
char *a=NULL;
char *b=NULL;
if (colonPairToStrings(colonPair,&a,&b))
    {
    if (a!=NULL)
	{
	if (first)
	    *first = atoi(a);
	freeMem(a);
	}
    if (b!=NULL)
	{
	if (second)
	    *second = atoi(b);
	freeMem(b);
	}
    return TRUE;
    }
return FALSE;
}

boolean colonPairToDoubles(char * colonPair,double *first,double *second)
{ // Non-destructive. Only sets values if found. No colon: value goes to *first
char *a=NULL;
char *b=NULL;
if (colonPairToStrings(colonPair,&a,&b))
    {
    if (a!=NULL)
	{
	if (first)
	    *first = strtod(a,NULL);
	freeMem(a);
	}
    if (b!=NULL)
	{
	if (second)
	    *second = strtod(b,NULL);
	freeMem(b);
	}
    return TRUE;
    }
return FALSE;
}

static void chopUpValues(filterBy_t *filterBy)
/* Chop up strings in filterBy or <column>FilterValues statement.   We look for optional labels
 * and optional CSS inside curly braces.
 */
{
struct slName *val = filterBy->slValues;
for (;val!=NULL;val=val->next)
    {
    // chip the style off the end of value or value|label
    char *chipper = strrchr(val->name,'{');
    if (chipper != NULL)
        {
        if (val == filterBy->slValues) // First one
            {
            filterBy->styleFollows = (lastChar(chipper) == '}');
            if (filterBy->styleFollows == FALSE) // Must be closed at the end of the string or
                filterBy->styleFollows = (*(chipper + 1) == '#'); // Legacy: color only
            }
        if (filterBy->styleFollows == FALSE)
            errAbort("filterBy values either all end in {CSS style} or none do.");
        *chipper++ = 0;  // delimit by null
        char *end = chipper + (strlen(chipper) - 1);
        if (*end == '}')
            *end = 0;
        else if (*(chipper + 1) != '#') // Legacy: Could be color only definition
            errAbort("filterBy values ending in style must be enclosed in {curly brackets}.");
        }
    else if (filterBy->styleFollows)
        errAbort("filterBy values either all end in {CSS style} or none do.");

    if (filterBy->useIndex)
        strSwapChar(val->name,'_',' '); // value is a label so swap underscores
    else
        {
        // now chip the label off the end of value name
        chipper =strchr(val->name,'|');
        if (chipper != NULL)
            {
            if (val == filterBy->slValues) // First one
                filterBy->valueAndLabel = TRUE;
            if (filterBy->valueAndLabel == FALSE)
                errAbort("filterBy values either all have labels (as value|label) "
                         "or none do.");
            *chipper++ = 0;  // The label is found inside filters->svValues as the next string
            strSwapChar(chipper,'_',' '); // Title does not have underscores
            }
        else if (filterBy->valueAndLabel)
            errAbort("filterBy values either all have labels in form of value|label "
                     "or none do.");
        }
    }
}

char *extractFieldNameNew(char *trackDbVariable, char *filterType)
/* Extract field name from a filter trackDb variable.  Variables are filter*.column */
{
char *ptr = strchr(trackDbVariable, '.');
if (ptr == NULL)
    errAbort("%s doesn't have a '.' in it", trackDbVariable);

return ptr + 1;
}

char *extractFieldNameOld(char *trackDbVariable, char *filterType)
/* Extract field name from a filter trackDb variable.  Variables can either be
 * <columnName>Filter* or <columnName>.Filter* */
{
char *field = cloneString(trackDbVariable);
int ix = strlen(field) - strlen(filterType); 
field[ix] = '\0';
if (field[ix - 1] == '.')
    field[ix - 1] = '\0';

return field;
}

static char *getFilterValueDefaultsSetting(struct cart *cart, struct trackDb *tdb, char *field)
// grab the default setting for a filterValues statement
{
char defaultsSetting[4096];
safef(defaultsSetting, sizeof defaultsSetting, "%s.%s", FILTER_VALUES_DEFAULT_NAME_LOW, field);
char *defaults = cartOrTdbString(cart, tdb, defaultsSetting, NULL);
if (defaults == NULL)
    {
    safef(defaultsSetting, sizeof defaultsSetting, "%s.%s", field, FILTER_VALUES_DEFAULT_NAME_CAP);
    defaults = cartOrTdbString(cart, tdb, defaultsSetting, NULL);
    }
if (defaults == NULL)
    {
    safef(defaultsSetting, sizeof defaultsSetting, "%s%s", field, FILTER_VALUES_DEFAULT_NAME_CAP);
    defaults = cartOrTdbString(cart, tdb, defaultsSetting, NULL);
    }
return defaults;
}

static char *getLabelSetting(struct cart *cart, struct trackDb *tdb, char *field)
{
char labelSetting[4096];
safef(labelSetting, sizeof labelSetting, "%s.%s", FILTER_LABEL_NAME_LOW, field);
char *trackDbLabel = cartOrTdbString(cart, tdb, labelSetting, NULL);
if (trackDbLabel == NULL)
    {
    safef(labelSetting, sizeof labelSetting, "%s.%s", field, FILTER_LABEL_NAME_CAP);
    trackDbLabel = cartOrTdbString(cart, tdb, labelSetting, NULL);
    }
if (trackDbLabel == NULL)
    {
    safef(labelSetting, sizeof labelSetting, "%s%s", field, FILTER_LABEL_NAME_CAP);
    trackDbLabel = cartOrTdbString(cart, tdb, labelSetting, NULL);
    }
return trackDbLabel;
}

static filterBy_t *buildFilterBy(struct trackDb *tdb, struct cart *cart, struct asObject *as, struct trackDbFilter *tdbFilter, char *name)
/* Build a filterBy_t structure from a <column>FilterValues statement. */
{
char *field = tdbFilter->fieldName;
if (isEmpty(tdbFilter->setting))
    errAbort("FilterValues setting of field '%s' must have a value.", tdbFilter->fieldName);

char *value = cartUsualStringClosestToHome(cart, tdb, FALSE, tdbFilter->name, tdbFilter->setting);

filterBy_t *filterBy;
AllocVar(filterBy);
filterBy->column = cloneString(field);
filterBy->title = cloneString(field); ///  title should come from AS file, or trackDb variable
struct asColumn *asCol = asColumnFind(as, field);
if (asCol != NULL)
    filterBy->title = asCol->comment;
else
    errAbort("Building filter on field %s which is not in AS file.", field);
char *trackDbLabel = getLabelSetting(cart, tdb, field);
if (trackDbLabel)
    filterBy->title = trackDbLabel;
filterBy->useIndex = FALSE;
filterBy->slValues = slNameListFromCommaEscaped(value);
chopUpValues(filterBy);
if (cart != NULL)
    {
    char suffix[256];
    safef(suffix, sizeof(suffix), "%s.%s", "filterBy", filterBy->column);
    boolean parentLevel = isNameAtParentLevel(tdb,tdb->track);
    if (cartLookUpVariableClosestToHome(cart,tdb,parentLevel,suffix,&(filterBy->htmlName)))
        {
        filterBy->slChoices = cartOptionalSlNameList(cart,filterBy->htmlName);
        freeMem(filterBy->htmlName);
        }
    }

if (filterBy->slChoices == NULL)  // no settings in cart, initialize from trackDb
    {
    char *setting = getFilterValueDefaultsSetting(cart, tdb, field);
    filterBy->slChoices = slNameListFromCommaEscaped(setting);
    }

struct dyString *dy = dyStringNew(128);
dyStringPrintf(dy, "%s.%s.%s", name, "filterBy", filterBy->column);
filterBy->htmlName = dy->string;

return filterBy;
}

filterBy_t *filterByValues(struct trackDb *tdb, struct cart *cart, struct trackDbFilter *trackDbFilters, char *name)
/* Build a filterBy_t list from tdb variables of the form *FilterValues */
{
struct asObject *as = asForTdb(NULL, tdb);
if (as == NULL)
    errAbort("Unable to get autoSql for %s", name);
filterBy_t *filterByList = NULL, *filter;
struct trackDbFilter *fieldFilter;
while ((fieldFilter = slPopHead(&trackDbFilters)) != NULL)
    {
    if ((filter = buildFilterBy(tdb, cart, as, fieldFilter, name)) != NULL)
        slAddHead(&filterByList, filter);
    }
return filterByList;
}

filterBy_t *filterBySetGetGuts(struct trackDb *tdb, struct cart *cart, char *name, char *subName, char *settingName)
// Gets one or more "filterBy" settings (ClosestToHome).  returns NULL if not found
{
// first check to see if this tdb is using "new" FilterValues cart variables
if (differentString(subName, "highlightBy"))
    {
    struct trackDbFilter *trackDbFilters = tdbGetTrackFilterByFilters( tdb);
    if (trackDbFilters)
        return filterByValues(tdb, cart, trackDbFilters, name);
    }

filterBy_t *filterBySet = NULL;
char *setting = trackDbSettingClosestToHome(tdb, settingName);
if(setting == NULL)
    return NULL;
if ( name == NULL )
    name = tdb->track;

setting = cloneString(setting);
char *filters[10];
// multiple filterBys are delimited by space but spaces inside filter can be protected "by quotes"
int filterCount = chopByWhiteRespectDoubleQuotes(setting, filters, ArraySize(filters));
int ix;
for (ix=0;ix<filterCount;ix++)
    {
    char *filter = cloneString(filters[ix]);
    filterBy_t *filterBy;
    AllocVar(filterBy);
    char *first = strchr(filter,':');
    if (first != NULL)
	*first = '\0';
    else
	errAbort("filterBySetGet() expected ':' divider between table column and label: %s", filters[ix]);
    filterBy->column = filter;
    filter += strlen(filter) + 1;
    first = strchr(filter,'=');
    if (first != NULL)
	*first = '\0';
    else
	errAbort("filterBySetGet() expected '=' divider between table column and options list: %s", filters[ix]);
    filterBy->title = strSwapChar(filter,'_',' '); // Title does not have underscores
    filter += strlen(filter) + 1;

    // Are values indexes to the string titles?
    if (filter[0] == '+')
	{
	filter += 1;
	filterBy->useIndex = TRUE;
	}

    // Now set up each of the values which may have 1-3 parts (value|label{style})
    // the slName list will have the 3 parts delimited by null value\0label\0style\0
    stripString(filter, "\"");  // Remove any double quotes now and chop by commmas
    filterBy->slValues = slNameListFromComma(filter);

    chopUpValues(filterBy);

    slAddTail(&filterBySet,filterBy); // Keep them in order (only a few)

    if (cart != NULL)
	{
	char suffix[256];
	safef(suffix, sizeof(suffix), "%s.%s", subName, filterBy->column);
	boolean parentLevel = isNameAtParentLevel(tdb,name);
	if (cartLookUpVariableClosestToHome(cart,tdb,parentLevel,suffix,&(filterBy->htmlName)))
	    {
	    filterBy->slChoices = cartOptionalSlNameList(cart,filterBy->htmlName);
	    freeMem(filterBy->htmlName);
	    }
	}

    // Note: cannot use found name above because that may be at a higher (composite/view) level
    int len = strlen(name) + strlen(filterBy->column) + 15;
    filterBy->htmlName = needMem(len);
    safef(filterBy->htmlName, len, "%s.%s.%s", name,subName,filterBy->column);
    }
freeMem(setting);

return filterBySet;
}

filterBy_t *filterBySetGet(struct trackDb *tdb, struct cart *cart, char *name)
/* Gets one or more "filterBy" settings (ClosestToHome).  returns NULL if not found */
{
return filterBySetGetGuts(tdb, cart, name, "filterBy", FILTER_BY);
}

filterBy_t *highlightBySetGet(struct trackDb *tdb, struct cart *cart, char *name)
/* Gets one or more "highlightBy" settings (ClosestToHome).  returns NULL if not found */
{
return filterBySetGetGuts(tdb, cart, name, "highlightBy", HIGHLIGHT_BY);
}

void filterBySetFree(filterBy_t **filterBySet)
// Free a set of filterBy structs
{
if (filterBySet != NULL)
    {
    while (*filterBySet != NULL)
	{
	filterBy_t *filterBy = slPopHead(filterBySet);
	if (filterBy->slValues != NULL)
	    slNameFreeList(filterBy->slValues);
	if (filterBy->slChoices != NULL)
	    slNameFreeList(filterBy->slChoices);
	if (filterBy->htmlName != NULL)
	    freeMem(filterBy->htmlName);
	freeMem(filterBy->column);
	freeMem(filterBy);
	}
    }
}

static char *filterByClauseStd(filterBy_t *filterBy)
// returns the SQL where clause for a single filterBy struct in the standard cases
{
int count = slCount(filterBy->slChoices);
struct dyString *dyClause = dyStringNew(256);
sqlDyStringPrintf(dyClause, "%s", filterBy->column);
if (count == 1)
    sqlDyStringPrintf(dyClause, " = ");
else
    sqlDyStringPrintf(dyClause, " in (");

struct slName *slChoice = NULL;
boolean first = TRUE;
for (slChoice = filterBy->slChoices;slChoice != NULL;slChoice=slChoice->next)
    {
    if (!first)
	sqlDyStringPrintf(dyClause, ",");
    first = FALSE;
    if (filterBy->useIndex)
	sqlDyStringPrintf(dyClause, "%d",atoi(slChoice->name));  // a number converted to a string
    else
	sqlDyStringPrintf(dyClause, "\"%s\"",slChoice->name);
    }
if (dyStringLen(dyClause) == 0)
    {
    dyStringFree(&dyClause);
    return NULL;
    }
if (count > 1)
    sqlDyStringPrintf(dyClause, ")");

return dyStringCannibalize(&dyClause);
}

char *filterByClause(filterBy_t *filterBy)
// returns the SQL where clause for a single filterBy struct
{
if (filterByAllChosen(filterBy))
    return NULL;
else
    return filterByClauseStd(filterBy);
}

struct dyString *dyAddFilterByClause(struct cart *cart, struct trackDb *tdb,
				 struct dyString *extraWhere,char *column, boolean *and)
// creates the where clause condition to support a filterBy setting.
// Format: filterBy column:Title=value,value [column:Title=value|label,value|label,value|label])
// filterBy filters are multiselect's so could have multiple values selected.
// thus returns the "column1 in (...) and column2 in (...)" clause.
// if 'column' is provided, and there are multiple filterBy columns, only the named column's
// clause is returned.
// The 'and' param and dyString in/out allows stringing multiple where clauses together
{
filterBy_t *filterBySet = filterBySetGet(tdb, cart,NULL);
if (filterBySet== NULL)
    return extraWhere;

filterBy_t *filterBy = filterBySet;
for (;filterBy != NULL; filterBy = filterBy->next)
    {
    if (column != NULL && differentString(column,filterBy->column))
	continue;

    char *clause = filterByClause(filterBy);
    if (clause != NULL)
	{
	if (*and)
	    sqlDyStringPrintf(extraWhere, " AND ");
	sqlDyStringPrintf(extraWhere,"%-s", clause);
	freeMem(clause);
	*and = TRUE;
	}
    }
filterBySetFree(&filterBySet);
return extraWhere;
}

char *filterBySetClause(filterBy_t *filterBySet)
// returns the "column1 in (...) and column2 in (...)" clause for a set of filterBy structs
{
struct dyString *dyClause = dyStringNew(256);
boolean notFirst = FALSE;
filterBy_t *filterBy = NULL;

for (filterBy = filterBySet;filterBy != NULL; filterBy = filterBy->next)
    {
    char *clause = filterByClause(filterBy);
    if (clause != NULL)
	{
	if (notFirst)
	    sqlDyStringPrintf(dyClause, " AND ");
	sqlDyStringPrintf(dyClause,"%-s", clause);
	freeMem(clause);
	notFirst = TRUE;
	}
    }
if (dyStringLen(dyClause) == 0)
    {
    dyStringFree(&dyClause);
    return NULL;
    }
return dyStringCannibalize(&dyClause);
}

void filterBySetCfgUiOption(filterBy_t *filterBy, struct slName *slValue, int ix)
/* output one option for filterBy UI  */
{
char varName[32];
char *label = NULL;
char *name = NULL;
if (filterBy->useIndex)
    {
    safef(varName, sizeof(varName), "%d",ix);
    name = varName;
    label = slValue->name;
    }
else
    {
    label = (filterBy->valueAndLabel? slValue->name + strlen(slValue->name)+1: slValue->name);
    name = slValue->name;
    }
printf("<OPTION");
if (filterBy->slChoices != NULL && slNameInList(filterBy->slChoices,name))
    printf(" SELECTED");
if (filterBy->useIndex || filterBy->valueAndLabel)
    printf(" value='%s'",name);
if (filterBy->styleFollows)
    {
    char *styler = label + strlen(label)+1;
    if (*styler != '\0')
	{
	if (*styler == '#') // Legacy: just the color that follows
	    printf(" style='color: %s;'",styler);
	else
	    printf(" style='%s'",styler);
	}
    }
printf(">%s</OPTION>\n",label);
}

static boolean filterByColumnIsMultiple(struct cart *cart, struct trackDb *tdb,  char *setting)
/* Is this filter setting expecting multiple items (e.g. has checkboxes in the UI) */
{
return (sameString(setting, FILTERBY_MULTIPLE) ||
        sameString(setting, FILTERBY_MULTIPLE_LIST_OR) ||
        sameString(setting, FILTERBY_MULTIPLE_LIST_ONLY_OR) ||
        sameString(setting, FILTERBY_MULTIPLE_LIST_ONLY_AND) ||
        sameString(setting, FILTERBY_MULTIPLE_LIST_AND));
}

static boolean advancedFilter(struct cart *cart, struct trackDb *tdb, char *setting)
{
if (!tdbIsBigBed(tdb))
    return FALSE;

return  sameString(setting, FILTERBY_MULTIPLE_LIST_OR) || sameString(setting, FILTERBY_MULTIPLE_LIST_AND);
}

void filterBySetCfgUiGuts(struct cart *cart, struct trackDb *tdb,
		      filterBy_t *filterBySet, boolean onOneLine,
		      char *filterTypeTitle, char *selectIdPrefix, char *allLabel, char *prefix)
// Does the UI for a list of filterBy structure for either filterBy or highlightBy controls
{
if (filterBySet == NULL)
    return;

#define FILTERBY_HELP_LINK "<A HREF=\"../goldenPath/help/multiView.html\" TARGET=ucscHelp>help</A>"
int count = slCount(filterBySet);
if (count == 1)
    puts("<TABLE class='trackUiFilterTable'><TR valign='top'>");
else
    printf("<B>%s items by:</B> (select multiple categories and items - %s)"
	   "<TABLE class='trackUiFilterTable'><TR valign='bottom'>\n",filterTypeTitle,FILTERBY_HELP_LINK);

#ifdef ADVANCED_BUTTON
if (tdbIsBigBed(tdb))
    {
    char varName[1024];
    safef(varName, sizeof(varName), "%s.doAdvanced", tdb->track);
    puts("&nbsp;&nbsp;&nbsp;");
    printf("<a id='%s' style='text-decoration: underline; color: #121E9A' title='Show advanced options..'>%s<img src='../images/downBlue.png'/></a>" ,varName,"Advanced ");
    printf("<BR>");
    jsInlineF("$(function () { advancedSearchOnChange('%s'); });\n", varName);
    }
#endif // ADVANCED_BUTTON


filterBy_t *filterBy = NULL;
if (cartOptionalString(cart, "ajax") == NULL)
    {
    webIncludeResourceFile("ui.dropdownchecklist.css");
    jsIncludeFile("ui.dropdownchecklist.js",NULL);
    jsIncludeFile("ddcl.js",NULL);
    }

// TODO: columnCount (Number of filterBoxes per row) should be configurable through tdb setting

for (filterBy = filterBySet;  filterBy != NULL;  filterBy = filterBy->next)
    {
    puts("<TD>");
    char selectStatement[4096];
    char *setting =  getFilterType(cart, tdb, filterBy->column, FILTERBY_DEFAULT);
    if (filterByColumnIsMultiple(cart, tdb, setting))
        safef(selectStatement, sizeof selectStatement, " (select multiple items - %s)", FILTERBY_HELP_LINK);
    else
        selectStatement[0] = 0;
    if(count == 1)
	printf("<B>%s by %s</B>%s",filterTypeTitle,filterBy->title,selectStatement);
    else
	printf("<B>%s</B>",filterBy->title);
    puts("</TD>");
    }
puts("</tr><tr>");
for (filterBy = filterBySet;  filterBy != NULL;  filterBy = filterBy->next)
    {
    puts("<td>");
    char *setting =  getFilterType(cart, tdb, filterBy->column, FILTERBY_DEFAULT);
    if (advancedFilter(cart, tdb, setting))
        {
        char cartSettingString[4096];
        safef(cartSettingString, sizeof cartSettingString, "%s.%s.%s", prefix,FILTER_TYPE_NAME_LOW, filterBy->column);
        printf("<div ><b>Match if  ");
        // ADVANCED BUTTON printf("<div class='advanced' style='display:none'><b>Match if  ");
        cgiMakeRadioButton(cartSettingString, FILTERBY_MULTIPLE_LIST_AND, sameString(setting, FILTERBY_MULTIPLE_LIST_AND));
        printf(" all ");
        cgiMakeRadioButton(cartSettingString, FILTERBY_MULTIPLE_LIST_OR, sameString(setting, FILTERBY_MULTIPLE_LIST_OR));
        printf(" one or more match</b></div> ");
        }
    puts("</td>");
    }
puts("</tr><tr>");
int ix=0;
for (filterBy = filterBySet;  filterBy != NULL;  filterBy = filterBy->next, ix++)
    {
    char *setting =  getFilterType(cart, tdb, filterBy->column, FILTERBY_DEFAULT);
    puts("<td>");
    // value is always "All", even if label is different, to simplify javascript code
    int valIx = 1;
    if (filterByColumnIsMultiple(cart, tdb, setting))
        printf( "<SELECT id='%s%d' name='%s' multiple style='display: none; font-size:.9em;' class='filterBy'><BR>\n", selectIdPrefix,ix,filterBy->htmlName);
    else
        printf( "<SELECT id='%s%d' name='%s' style='font-size:.9em;'<BR>\n", selectIdPrefix,ix,filterBy->htmlName);

    printf("<OPTION%s value=\"All\">%s</OPTION>\n", (filterByAllChosen(filterBy)?" SELECTED":""), allLabel);
    struct slName *slValue;

    for (slValue=filterBy->slValues;slValue!=NULL;slValue=slValue->next,valIx++)
	{
	char varName[32];
	char *label = NULL;
	char *name = NULL;
	if (filterBy->useIndex)
	    {
	    safef(varName, sizeof(varName), "%d",valIx);
	    name = varName;
	    label = slValue->name;
	    }
	else
	    {
	    label = (filterBy->valueAndLabel ? slValue->name + strlen(slValue->name)+1
					     : slValue->name);
	    name = slValue->name;
	    }
	printf("<OPTION");
	if (filterBy->slChoices != NULL && slNameInList(filterBy->slChoices,name))
	    printf(" SELECTED");
	if (filterBy->useIndex || filterBy->valueAndLabel)
	    printf(" value='%s'",name);
	if (filterBy->styleFollows)
	    {
	    char *styler = label + strlen(label)+1;
	    if (*styler != '\0')
		{
		if (*styler == '#') // Legacy: just the color that follows
		    printf(" style='color: %s;'",styler);
		else
		    printf(" style='%s'",styler);
		}
	    }
	printf(">%s</OPTION>\n",label);
	}
    printf("</SELECT>\n");
    puts("</td>");
    }

puts("</TR></TABLE>");
}

void filterBySetCfgUi(struct cart *cart, struct trackDb *tdb,
		  filterBy_t *filterBySet, boolean onOneLine, char *prefix)
/* Does the filter UI for a list of filterBy structure */
{
filterBySetCfgUiGuts(cart, tdb, filterBySet, onOneLine, "Filter", "fbc", "All", prefix);
}

void highlightBySetCfgUi(struct cart *cart, struct trackDb *tdb,
		     filterBy_t *filterBySet, boolean onOneLine, char *prefix)
/* Does the highlight UI for a list of filterBy structure */
{
filterBySetCfgUiGuts(cart, tdb, filterBySet, onOneLine, "Highlight", "hbc", "None", prefix);
}

#define COLOR_BG_DEFAULT_IX     0
#define COLOR_BG_ALTDEFAULT_IX  1
#define DIVIDING_LINE "<TR valign=\"CENTER\" line-height=\"1\" BGCOLOR=\"%s\"><TH colspan=\"5\" " \
		  "align=\"CENTER\"><hr noshade color=\"%s\" width=\"100%%\"></TD></TR>\n"
#define DIVIDER_PRINT(color) printf(DIVIDING_LINE,COLOR_BG_DEFAULT,(color))

static boolean divisionIfNeeded(char **lastDivide,dividers_t *dividers,membership_t *membership)
// Keeps track of location within subtracks in order to provide requested division lines
{
boolean division = FALSE;
if (dividers)
    {
    if (lastDivide != NULL)
	{
	int ix;
	for (ix=0;ix<dividers->count;ix++)
	    {
	    int sIx = stringArrayIx(dividers->subgroups[ix],membership->subgroups,
				    membership->count);
	    if ((lastDivide[ix] == (void*)NULL && sIx >= 0)
	    ||  (lastDivide[ix] != (void*)NULL && sIx <  0)
	    ||  (strcmp(lastDivide[ix],membership->membership[sIx]) != 0) )
		{
		division = TRUE;
		if (lastDivide[ix] != (void*)NULL)
		    freeMem(lastDivide[ix]);
		lastDivide[ix] = (sIx<0 ? (void*)NULL : cloneString(membership->membership[sIx]));
		}
	    }
	}
    //if (division)
    //    DIVIDER_PRINT(COLOR_DARKGREEN);
    }
return division;
}

static void indentIfNeeded(hierarchy_t*hierarchy,membership_t *membership)
// inserts any needed indents
{
int indent = 0;
if (hierarchy && hierarchy->count>0)
    {
    int ix;
    for (ix=0;ix<membership->count;ix++)
	{
	int iIx = stringArrayIx(membership->membership[ix], hierarchy->membership,
				hierarchy->count);
	if (iIx >= 0)
	    {
	    indent = hierarchy->indents[iIx];
	    break;  // Only one
	    }
	}
    }
for (;indent>0;indent--)
    puts("&nbsp;&nbsp;&nbsp;");
}

// FIXME FIXME Should be able to use membersForAll struct to set default sort order from subGroups
// FIXME FIXME This should be done in hgTrackDb at load time and should change tag values to
// FIXME FIXME ensure js still works
boolean tdbAddPrioritiesFromCart(struct cart *cart, struct trackDb *tdbList)
// Updates the tdb->priority from cart for all tracks in list and their descendents.
{
char htmlIdentifier[1024];
struct trackDb *tdb;
boolean cartPriorities = FALSE;
for (tdb = tdbList; tdb != NULL; tdb = tdb->next)
    {
    safef(htmlIdentifier, sizeof(htmlIdentifier), "%s.priority", tdb->track);
    char *cartHas = cartOptionalString(cart,htmlIdentifier);
    if (cartHas != NULL)
	{
	tdb->priority = atof(cartHas);
	cartPriorities = TRUE;
	}
    if (tdbAddPrioritiesFromCart(cart, tdb->subtracks))
	cartPriorities = TRUE;
    }
return cartPriorities;
}

boolean tdbSortPrioritiesFromCart(struct cart *cart, struct trackDb **tdbList)
// Updates the tdb->priority from cart then sorts the list anew.
// Returns TRUE if priorities obtained from cart
{
boolean cartPriorities = tdbAddPrioritiesFromCart(cart, *tdbList);
slSort(tdbList, trackDbCmp);
return cartPriorities;
}

boolean tdbRefSortPrioritiesFromCart(struct cart *cart, struct slRef **tdbRefList)
// Updates the tdb->priority from cart then sorts the list anew.
// Returns TRUE if priorities obtained from cart
{
char htmlIdentifier[128];
struct slRef *tdbRef;
boolean cartPriorities = FALSE;
for (tdbRef = *tdbRefList; tdbRef != NULL; tdbRef = tdbRef->next)
    {
    struct trackDb *tdb = tdbRef->val;
    safef(htmlIdentifier, sizeof(htmlIdentifier), "%s.priority", tdb->track);
    char *cartHas = cartOptionalString(cart,htmlIdentifier);
    if (cartHas != NULL)
	{
	tdb->priority = atof(cartHas);
	cartPriorities = TRUE;
	}
    }
slSort(tdbRefList, trackDbRefCmp);
return cartPriorities;
}

void bigRmskCfgUi(char *db, struct cart *cart, struct trackDb *tdb, char *name, char *title, boolean boxed)
/* UI for the bigRmsk track */
{
boxed = cfgBeginBoxAndTitle(tdb, boxed, title);
boolean showUnalignedExtents = cartUsualBoolean(cart, BIGRMSK_SHOW_UNALIGNED_EXTENTS, BIGRMSK_SHOW_UNALIGNED_EXTENTS_DEFAULT);
boolean showLabels = cartUsualBoolean(cart, BIGRMSK_SHOW_LABELS, BIGRMSK_SHOW_LABELS_DEFAULT);
boolean origPackViz = cartUsualBoolean(cart, BIGRMSK_ORIG_PACKVIZ, BIGRMSK_ORIG_PACKVIZ_DEFAULT);
boolean regexpFilter = cartUsualBoolean(cart, BIGRMSK_REGEXP_FILTER, BIGRMSK_REGEXP_FILTER_DEFAULT);
char * nameFilter = cartUsualString(cart, BIGRMSK_NAME_FILTER, BIGRMSK_NAME_FILTER_DEFAULT);
puts("<br>");
printf("<b>Filter by 'name#class/subclass' field (e.g. '*Alu*' would match 'FRAM#SINE/Alu'):</b> ");
cgiMakeTextVar(BIGRMSK_NAME_FILTER, cartUsualString(cart, BIGRMSK_NAME_FILTER, nameFilter), 20);
cgiMakeCheckBox(BIGRMSK_REGEXP_FILTER, regexpFilter);
puts("&nbsp;<B>regular expression</B></P>");
cgiMakeCheckBox(BIGRMSK_SHOW_UNALIGNED_EXTENTS, showUnalignedExtents);
puts("&nbsp;<B>Show unaligned repeat regions</B></P>");
cgiMakeCheckBox(BIGRMSK_SHOW_LABELS, showLabels);
puts("&nbsp;<B>Show annotation labels</B></P>");
cgiMakeCheckBox(BIGRMSK_ORIG_PACKVIZ, origPackViz);
puts("&nbsp;<B>Display original pack visualization</B></P>");
cfgEndBox(boxed);
}

void lollyCfgUi(char *db, struct cart *cart, struct trackDb *tdb, char *name, char *title, boolean boxed)
/* UI for the wiggle track */
{
int maxHeightPixels;
int minHeightPixels;
char option[256];
int defaultHeight;  /*  pixels per item */
int settingsDefault;

#define MIN_HEIGHT_LOLLY        32
cartTdbFetchMinMaxPixels(cart, tdb, MIN_HEIGHT_LOLLY, atoi(DEFAULT_HEIGHT_PER), atoi(DEFAULT_HEIGHT_PER),
                                &minHeightPixels, &maxHeightPixels, &settingsDefault, &defaultHeight);

boxed = cfgBeginBoxAndTitle(tdb, boxed, title);
printf("<TABLE BORDER=0>");

printf("<TR valign=middle><th align=right>Track height:</th><td align=left colspan=3>");
safef(option, sizeof(option), "%s.%s", name, HEIGHTPER );
cgiMakeIntVarWithLimits(option, defaultHeight, "Track height",0, minHeightPixels, maxHeightPixels);
printf("pixels&nbsp;(range: %d to %d)",
       minHeightPixels, maxHeightPixels);

char *autoScale;
wigFetchAutoScaleWithCart(cart,tdb,name, &autoScale);

printf("<TR valign=middle><th align=right>Data view scaling:</th><td align=left colspan=3>");
safef(option, sizeof(option), "%s.%s", name, AUTOSCALE );
wiggleScaleDropDown(option, autoScale);
void wiggleScaleDropDownJavascript(char *name);
wiggleScaleDropDownJavascript(name);
puts("</TD></TR>");

double minY;        /*  from trackDb or cart    */
double maxY;        /*  from trackDb or cart    */
double tDbMinY;     /*  data range limits from trackDb type line */
double tDbMaxY;     /*  data range limits from trackDb type line */
char *words[8];     /*  to parse the trackDb type line  */
int wordCount = 0;  /*  to parse the trackDb type line  */
wigFetchMinMaxYWithCart(cart, tdb, name, &minY, &maxY, &tDbMinY, &tDbMaxY, wordCount, words);
printf("<TR class=\"%sAutoScaleDesc\" valign=middle><th align=right>Vertical viewing range:</th>"
       "<td align=left>&nbsp;min:&nbsp;", name);
safef(option, sizeof(option), "%s.%s", name, MIN_Y );
cgiMakeDoubleVarWithLimits(option, minY, "Range min", 0, NO_VALUE, NO_VALUE);
printf("</td><td align=leftv colspan=2>max:&nbsp;");
safef(option, sizeof(option), "%s.%s", name, MAX_Y );
cgiMakeDoubleVarWithLimits(option, maxY, "Range max", 0, NO_VALUE, NO_VALUE);
/*
printf("<TR valign=middle><th align=right>Drawing method:</th><td align=left>");
safef(option, sizeof(option), "%s.%s", name, POPMETHOD);
char *popMethodVal = cartOrTdbString(cart, tdb, "popMethod", NULL);
        
cgiMakeDropListFull(option, popMethodLabels, popMethodValues,
    ArraySize(popMethodValues), popMethodVal, NULL, NULL);
    */

puts("</td></TR>");

printf("</TABLE>");
cfgEndBox(boxed);
}

void labelMakeCheckBox(struct cart *cart, struct trackDb *tdb, char *sym, char *desc,
                       boolean defaultOn)
/* add a checkbox for the user to select a component of a label (e.g. ID, name, other info).
 * NOTE: This does not have a track name argument, so the correct tdb must be passed in:
 * if setting is at composite level, then pass in composite tdb, likewise for view. */
{
char suffix[512];
safef(suffix, sizeof(suffix), "label.%s", sym);
boolean option = cartUsualBooleanClosestToHome(cart, tdb, FALSE, suffix, defaultOn);
char cartVar[1024];
safef(cartVar, sizeof cartVar, "%s.%s", tdb->track, suffix);
cgiMakeCheckBox(cartVar, option);
printf(" %s&nbsp;&nbsp;&nbsp;", desc);
}

void geneTrackMakeCheckBox(struct cart *cart, struct trackDb *tdb, char *sym, char *desc,
                       boolean defaultOn)
/* add a checkbox for the user to select a component of a label (e.g. ID, name, other info).
 * NOTE: This does not have a track name argument, so the correct tdb must be passed in:
 * if setting is at composite level, then pass in composite tdb, likewise for view. */
{
char suffix[512];
safef(suffix, sizeof(suffix), "geneTrack.%s", sym);
boolean option = cartUsualBooleanClosestToHome(cart, tdb, FALSE, suffix, defaultOn);
char cartVar[1024];
safef(cartVar, sizeof cartVar, "%s.%s", tdb->track, suffix);
cgiMakeCheckBox(cartVar, option);
printf(" %s&nbsp;&nbsp;&nbsp;", desc);
}

static void freqSourceSelect(struct cart *cart, struct trackDb *tdb, char *name)
/* Make a select input for preferred source of allele frequencies from
 * trackDb setting freqSourceOrder. */
{
char *freqSourceOrder = cloneString(trackDbSetting(tdb, "freqSourceOrder"));
if (isEmpty(freqSourceOrder))
    return;
int fsCount = countSeparatedItems(freqSourceOrder, ',');
char *values[fsCount];
chopCommas(freqSourceOrder, values);
char *menu[fsCount];
int i;
for (i = 0; i < fsCount;  i++)
    {
    // Change label of GnomAD to "GnomAD genomes" for clarity when "GnomAD_exomes" is present.
    if (sameString(values[i], "GnomAD") && stringIx("GnomAD_exomes", values) >= 0)
        menu[i] = "GnomAD genomes";
    else
        {
        menu[i] = cloneString(values[i]);
        strSwapChar(menu[i], '_', ' ');
        }
    }
boolean parentLevel = isNameAtParentLevel(tdb, name);
char *freqProj = cartOptionalStringClosestToHome(cart, tdb, parentLevel, "freqProj");
puts("<b>Frequency source/project to use for Minor Allele Frequency (MAF):</b>");
char cartVar[1024];
safef(cartVar, sizeof cartVar, "%s.freqProj", name);
cgiMakeDropListWithVals(cartVar, menu, values, ArraySize(menu), freqProj);
puts("<br>");
}

struct trackDb *tdbOrAncestorByName(struct trackDb *tdb, char *name)
/* For reasons Angie cannot fathom, if a composite or view is passed to cfgByCfgType then
 * cfgByCfgType passes a leaf subtrack to its callees like bigDbSnpCfgUi.  That is why we
 * see so many calls to isNameAtParentLevel, which returns true if the tdb was originally
 * at the composite or view level, which we can only tell by comparing with the original track name.
 * labelMakeCheckBox, called by many handlers in hgTrackUi that must be always top-level
 * (or have a special handler that bypasses cfgByCfgType like refSeqComposite),
 * is blissfully unaware of this.  It uses the same tdb for looking in cart ClosestToHome
 * and for making the HTML element's cart var name, trusting that the correct tdb has been
 * handed to it.
 * So in order for a callee of cfgByCfgType to call labelMakeCheckBox with the correct tdb,
 * we need to walk back up comparing name like isNameAtParentLevel does.
 * If name doesn't match tdb or any of its ancestors then this returns NULL. */
{
struct trackDb *correctTdb;
for (correctTdb = tdb;  correctTdb != NULL;  correctTdb = correctTdb->parent)
    if (startsWithWordByDelimiter(correctTdb->track, '.', name))
        return correctTdb;
return NULL;
}

void snp153MakeCheckboxGroupSetClearButton(char *buttonVar, boolean isSet)
/* Make a button for setting or clearing a set of checkboxes with the same name.
 * Uses only javascript to change the checkboxes, no resubmit. */
{
char id[256];
char javascript[256];
safef(javascript, sizeof(javascript), 
	"$(\"input:checkbox[name^='%s']\").each(function(){this.checked = %s;$(this).trigger('change');})",
	buttonVar, isSet ? "true" : "false");
safef(id, sizeof id, "%s_grp%sBut", buttonVar, isSet ? "Set" : "Clr");
cgiMakeOnClickButton(id, javascript, isSet ? JS_SET_ALL_BUTTON_LABEL : JS_CLEAR_ALL_BUTTON_LABEL);
}


struct trackDb *snp125FetchGeneTracks(char *database, struct cart *cart)
/* Get a list of genePred tracks. */
{
struct trackDb *tdbList = NULL;
struct hash *trackHash = trackHashMakeWithComposites(database,NULL,&tdbList,FALSE);

struct sqlConnection *conn = hAllocConn(database);
struct slName *justGenePredTables = hTrackTablesOfType(conn, "genePred%%"), *gt;
struct slName *bigGenePredTables = hTrackTablesOfType(conn, "bigGenePred%%");
struct slName *genePredTables = slCat(justGenePredTables,bigGenePredTables);
struct trackDb *geneTdbList = NULL, *gTdb;
if (genePredTables != NULL)
    {
    for (gt = genePredTables;  gt != NULL;  gt = gt->next)
	{
	gTdb = hashFindVal(trackHash, gt->name);
	if (gTdb && sameString(gTdb->grp, "genes"))
	    {
	    // We are going to overwrite gTdb's next pointer and possibly its priority,
	    // so make a shallow copy:
	    gTdb = CloneVar(gTdb);
	    if (gTdb->parent)
		gTdb->priority = (gTdb->parent->priority + gTdb->priority/1000);
	    slAddHead(&geneTdbList, gTdb);
	    }
	}
    slSort(&geneTdbList, trackDbCmp);
    }
hFreeConn(&conn);
return geneTdbList;
}

void snp153OfferGeneTracksForFunction(char *database, struct cart *cart, char *name, struct trackDb *tdb, struct trackDb *correctTdb)
/* Get a list of genePred tracks and make checkboxes to enable hgc's functional
 * annotations. */
{
struct trackDb *geneTdbList = NULL, *gTdb;
geneTdbList = snp125FetchGeneTracks(database, cart);
if (geneTdbList)
    {

    char sectionVar[256];
    safef(sectionVar, sizeof(sectionVar), "%s.geneTracks", name);
    jsBeginCollapsibleSection(cart, name, sectionVar,
			      "Use Gene Tracks for Functional Annotation", FALSE);
    printf("<BR><B>On details page, show function and coding differences relative to: </B>\n");

    char buttonVarPrefix[256];
    safef(buttonVarPrefix, sizeof(buttonVarPrefix), "%s.geneTrack", name);
    snp153MakeCheckboxGroupSetClearButton(buttonVarPrefix, TRUE);
    snp153MakeCheckboxGroupSetClearButton(buttonVarPrefix, FALSE);

    struct slName *defaultGeneTracks = slNameListFromComma(trackDbSetting(tdb, "defaultGeneTracks"));

    int i;
    int numCols = 4;
    int menuSize = slCount(geneTdbList);
    char **values = needMem(menuSize*sizeof(char *));
    char **labels = needMem(menuSize*sizeof(char *));
    for (i = 0, gTdb = geneTdbList;  i < menuSize && gTdb != NULL;  i++, gTdb = gTdb->next)
	{
	values[i] = gTdb->track;
	if (gTdb->parent != NULL)
	    {
	    struct dyString *dy = dyStringNew(0);
	    if (gTdb->parent->parent != NULL &&
		!startsWith(gTdb->parent->parent->shortLabel, gTdb->parent->shortLabel))
		dyStringPrintf(dy, "%s: ", gTdb->parent->parent->shortLabel);
	    if (!startsWith(gTdb->parent->shortLabel, gTdb->shortLabel))
		dyStringPrintf(dy, "%s: ", gTdb->parent->shortLabel);
	    dyStringPrintf(dy, "%s", gTdb->shortLabel);
	    labels[i] = dyStringCannibalize(&dy);
	    }
	else
	    labels[i] = gTdb->shortLabel;
	}

    printf("<BR>\n");

    puts("<TABLE BORDERWIDTH=0><TR>");
    for (i = 0; i < menuSize; ++i)
	{
	if (i > 0 && (i % numCols) == 0)
	    printf("</TR><TR>");
	printf("<TD>");
	geneTrackMakeCheckBox(cart, correctTdb, values[i], labels[i], slNameInList(defaultGeneTracks, values[i]) );
        printf("</TD>");
	}
    if ((i % numCols) != 0)
	while ((i++ % numCols) != 0)
	    printf("<TD></TD>");
    puts("</TR></TABLE>");

    jsEndCollapsibleSection();

    }
}

void bigDbSnpCfgUi(char *db, struct cart *cart, struct trackDb *leafTdb, char *name, char *title,
                   boolean boxed)
/* UI for bigDbSnp a.k.a. "dbSNP 2.0". */
{
boxed = cfgBeginBoxAndTitle(leafTdb, boxed, title);
freqSourceSelect(cart, leafTdb, name);
puts("<br>");
puts("<b>Label:</b>");
struct trackDb *correctTdb = tdbOrAncestorByName(leafTdb, name);
labelMakeCheckBox(cart, correctTdb, "rsId", "rs# identifier", TRUE);
labelMakeCheckBox(cart, correctTdb, "refAlt", "reference/alternate allele", TRUE);
labelMakeCheckBox(cart, correctTdb, "majMin", "major/minor allele", FALSE);
labelMakeCheckBox(cart, correctTdb, "maf", "MAF if available", FALSE);
labelMakeCheckBox(cart, correctTdb, "func", "Most severe functional impact on gene if any", FALSE);
puts("<br>");
scoreCfgUi(db, cart, leafTdb, name, "", 0, FALSE);
puts("For more information about the &quot;Interesting or anomalous properties&quot;, "
     "see <a href='#ucscNotes'>below</a>.");
puts("<br><br>");
puts("<b>Minimum MAF:</b>");
boolean parentLevel = isNameAtParentLevel(leafTdb, name);
double minMaf = cartUsualDoubleClosestToHome(cart, leafTdb, parentLevel, "minMaf", 0.0);
char cartVar[1024];
safef(cartVar, sizeof cartVar, "%s.minMaf", name);
cgiMakeDoubleVarWithLimits(cartVar, minMaf, "MAF", 0, 0.0, 0.5);
puts("range: 0.0 - 0.5<br>");

// Make wrapper table for collapsible sections:
puts("<TABLE border=0 cellspacing=0 cellpadding=0>");

snp153OfferGeneTracksForFunction(db, cart, name, leafTdb, correctTdb);

// End wrapper table for collapsible sections:
puts("</TABLE>");

wigOption(cart, name, title, leafTdb);

cfgEndBox(boxed);
}

void cfgByCfgType(eCfgType cType,char *db, struct cart *cart, struct trackDb *tdb,char *prefix,
	      char *title, boolean boxed)
// Methods for putting up type specific cfgs used by composites/subtracks in hui.c
{
// When only one subtrack, then show it's cfg settings instead of composite/view level settings
// This simplifies the UI where hgTrackUi won't have 2 levels of cfg,
// while hgTracks still supports rightClick cfg of the subtrack.

if (configurableByAjax(tdb,cType) > 0) // Only if subtrack's configurable by ajax do we
    {                                  // consider this option
    if (tdbIsComposite(tdb)                       // called for the composite
        && !isCustomComposite(tdb)
        && !tdbIsCompositeView(tdb->subtracks)        // and there is no view level
        && slCount(tdb->subtracks) == 1)              // and there is only one subtrack
	{
	tdb = tdb->subtracks; // show subtrack cfg instead
	prefix = tdb->track;
	}
    else if (tdbIsSubtrack(tdb)                   // called with subtrack
	 && tdbIsCompositeView(tdb->parent)       // subtrack has view
	 && differentString(prefix,tdb->track)    // and this has been called FOR the view
	 && slCount(tdb->parent->subtracks) == 1) // and view has only one subtrack
	prefix = tdb->track; // removes reference to view level
    }

// Cfg could be explicitly blocked, but if tdb is example subtrack
// then blocking should have occurred before we got here.
if (!tdbIsSubtrack(tdb) && trackDbSettingBlocksConfiguration(tdb,FALSE))
    return;

// composite/view must pass in example subtrack
// NOTE: if subtrack types vary then there shouldn't be cfg at composite/view level!
while (tdb->subtracks)
tdb = tdb->subtracks;

switch(cType)
    {
    case cfgBedScore:
			{
			char *scoreMax = trackDbSettingClosestToHome(tdb, SCORE_FILTER _MAX);
			int maxScore = (scoreMax ? sqlUnsigned(scoreMax):1000);
			scoreCfgUi(db, cart,tdb,prefix,title,maxScore,boxed);

            if(startsWith("bigBed", tdb->type))
                {
                labelCfgUi(db, cart, tdb, prefix);
                mergeSpanCfgUi(cart, tdb, prefix);
                wigOption(cart, prefix, title, tdb);
                }
			}
			break;
    case cfgPeak:
			encodePeakCfgUi(cart,tdb,prefix,title,boxed);
			break;
    case cfgWig:        wigCfgUi(cart,tdb,prefix,title,boxed);
			break;
    case cfgWigMaf:     wigMafCfgUi(cart,tdb,prefix,title,boxed, db);
			break;
    case cfgGenePred:   genePredCfgUi(db, cart,tdb,prefix,title,boxed);
			break;
    case cfgChain:      chainCfgUi(db,cart,tdb,prefix,title,boxed, NULL);
			break;
    case cfgNetAlign:   netAlignCfgUi(db,cart,tdb,prefix,title,boxed);
			break;
    case cfgBedFilt:    bedFiltCfgUi(cart,tdb,prefix,title, boxed);
			break;
    case cfgBam:        bamCfgUi(cart, tdb, prefix, title, boxed);
			break;
    case cfgVcf:        vcfCfgUi(cart, tdb, prefix, title, boxed);
			break;
    case cfgLong:       longRangeCfgUi(cart, tdb, prefix, title, boxed);
			break;
    case cfgSnake:      snakeCfgUi(cart, tdb, prefix, title, boxed);
			break;
    case cfgPsl:        pslCfgUi(db,cart,tdb,prefix,title,boxed);
                        break;
    case cfgBarChart:   barChartCfgUi(db,cart,tdb,prefix,title,boxed);
                        break;
    case cfgInteract:   interactCfgUi(db,cart,tdb,prefix,title,boxed);
                        break;
    case cfgBigRmsk:    bigRmskCfgUi(db,cart,tdb,prefix,title,boxed);
                        break;
    case cfgLollipop:   lollyCfgUi(db,cart,tdb,prefix,title,boxed);
			scoreCfgUi(db, cart,tdb,prefix,title,1000,boxed);
                        break;
    case cfgHic:        hicCfgUi(db,cart,tdb,prefix,title,boxed);
                        break;
    case cfgBigDbSnp:   bigDbSnpCfgUi(db, cart, tdb, prefix, title, boxed);
                        break;
    default:            warn("Track type is not known to multi-view composites. type is: %d ",
			     cType);
			break;
    }
}

char *encodeRestrictionDate(char *db,struct trackDb *trackDb,boolean excludePast)
// Create a string for ENCODE restriction date of this track
// if return is not null, then free it after use
{
if (!trackDb)
    return NULL;

char *date = NULL;

if (metadataForTable(db,trackDb,NULL) != NULL)
    {
    date = cloneString((char *)metadataFindValue(trackDb,"dateUnrestricted"));
    if (date != NULL)
	date = strSwapChar(date, ' ', 0);   // Truncate time (not expected, but just in case)

    if (excludePast && !isEmpty(date) && dateIsOld(date, MDB_ENCODE_DATE_FORMAT))
	freez(&date);
    }
return date;
}

/* Subtrack configuration settings */

struct subtrackConfigSettings 
    {
    sortOrder_t *sortOrder; /* from trackDb setting */
    boolean useDragAndDrop; /* from trackDb setting */
    boolean restrictions;   /* from metadata ? */
    boolean colorPatch;     /* from trackDb setting */
    boolean displayAll;     /* from radiobutton */
    int bgColorIx;          /* from logic over other settings */
    int columnCount;        /* determined from trackDb settings */
    };
#define LARGE_COMPOSITE_CUTOFF 30

static void printSubtrackListRadioButtons(char *parentTrack, int subCount, boolean displayAll)
// Print radio buttons for all/select
{
printf("<B>List subtracks:&nbsp;");
char javascript[JBUFSIZE];
safef(javascript, sizeof(javascript),
      "showOrHideSelectedSubtracks(true);");
char buffer[SMALLBUF];
if (subCount > LARGE_COMPOSITE_CUTOFF)
    safef(buffer,SMALLBUF,"%s.displaySubtracks", parentTrack);
else
    safecpy(buffer,SMALLBUF,"displaySubtracks");
cgiMakeOnEventRadioButtonWithClass(buffer, "selected", !displayAll, "allOrOnly", "click", javascript);
puts("only selected/visible &nbsp;&nbsp;");
safef(javascript, sizeof(javascript),
      "showOrHideSelectedSubtracks(false);");
cgiMakeOnEventRadioButtonWithClass(buffer, "all", displayAll, "allOrOnly", "click", javascript);
printf("all</B>");
if (subCount > 5)
    printf("&nbsp;&nbsp;&nbsp;&nbsp;(<span class='subCBcount'></span>)");
}

static void printSubtrackTableHeader(struct trackDb *parentTdb, struct slRef *subtrackRefList,
				struct subtrackConfigSettings *settings)
/* Print header of subtrack table, including classes describing display appearance and behavior.
Return number of columns */
{
boolean useDragAndDrop = settings->useDragAndDrop;
sortOrder_t *sortOrder = settings->sortOrder;
if (sortOrder != NULL)
    puts("<THEAD class=sortable>");
else
    puts("<THEAD>");
int colspan = 3;
if (sortOrder != NULL)
    colspan = sortOrder->count+2;
else if (!tdbIsMultiTrack(parentTdb)) // An extra column for subVis/wrench so dragAndDrop works
    colspan++;
if (settings->colorPatch)
    colspan++;
int columnCount = 0;
if (sortOrder != NULL)
    printf("<TR id=\"subtracksHeader\" class='sortable%s'>\n",
		useDragAndDrop ? " nodrop nodrag" : "");
else
    {
    printf("<TR%s>", useDragAndDrop ? " id='noDrag' class='nodrop nodrag'" : "");
    // First table row contains the display "selected/visible" or "all" radio buttons
    // NOTE: list subtrack radio buttons are inside tracklist table header if
    //       there are no sort columns.  The reason is to ensure spacing of lines
    //       column headers when the only column header is "Restricted Until"
    printf("<TD colspan='%d'>", colspan);
    int subCount = slCount(subtrackRefList);
    printSubtrackListRadioButtons(parentTdb->track, subCount, settings->displayAll);
    puts("</TD>");
    columnCount = colspan;
    }

// Add column headers which are sort button links
if (sortOrder != NULL)
    {
    printf("<TH>&nbsp;<INPUT TYPE=HIDDEN NAME='%s' class='sortOrder' VALUE='%s'></TH>\n",
	   sortOrder->htmlId, sortOrder->sortOrder); // keeing track of sortOrder
    columnCount++;
    if (!tdbIsMultiTrack(parentTdb))  // An extra column for subVis/wrench so dragAndDrop works
	{
	printf("<TH></TH>\n");
	columnCount++;
	}
    // Columns in tdb order (unchanging), sort in cart order (changed by user action)
    int sIx=0;
    for (sIx=0;sIx<sortOrder->count;sIx++)
	{
	if (sameString(SORT_ON_TRACK_NAME,sortOrder->column[sIx]))
	    break; // All wrangler requested sort orders have been done.
	if (sameString(SORT_ON_RESTRICTED,sortOrder->column[sIx]))
	    break; // All wrangler requested sort orders have been done.
	printf("<TH id='%s' class='sortable%s sort%d' abbr='use'>%s", 
		sortOrder->column[sIx],
	       (sortOrder->forward[sIx] ? "" : " sortRev"),sortOrder->order[sIx],
	       sortOrder->title[sIx]);
	jsOnEventById("click", sortOrder->column[sIx], "tableSortAtButtonPress(this);");
	printf("<sup>%s",(sortOrder->forward[sIx]?"&darr;":"&uarr;"));
	if (sortOrder->count > 1)
	    printf("%d",sortOrder->order[sIx]);
	printf("</sup>");
	puts("</TH>");
	columnCount++;
	}

    // longLabel column
    assert(sameString(SORT_ON_TRACK_NAME,sortOrder->column[sIx]));
    printf("<TH id='%s' class='sortable%s sort%d' align='left'>&nbsp;&nbsp;Track Name",
	   sortOrder->column[sIx],(sortOrder->forward[sIx]?"":" sortRev"),sortOrder->order[sIx]);
    jsOnEventById("click", sortOrder->column[sIx], "tableSortAtButtonPress(this);");
    printf("<sup>%s%d</sup>",(sortOrder->forward[sIx]?"&darr;":"&uarr;"),sortOrder->order[sIx]);
    puts("</TH>");
    columnCount++;
    }
puts("<TH>&nbsp;</TH>"); // schema column
columnCount++;

// Finally there may be a restricted until column
if (settings->restrictions)
    {
    if (sortOrder != NULL)
	{
	int sIx=sortOrder->count-1;
	assert(sameString(SORT_ON_RESTRICTED,sortOrder->column[sIx]));
	printf("<TH id='%s' class='sortable%s sort%d' align='left'>&nbsp;Restricted Until", 
		sortOrder->column[sIx],
		(sortOrder->forward[sIx]?"":" sortRev"),sortOrder->order[sIx]);
	jsOnEventById("click", sortOrder->column[sIx], "tableSortAtButtonPress(this);");
	printf("<sup>%s%d</sup>",(sortOrder->forward[sIx] ? "&darr;" : "&uarr;"),
	       sortOrder->order[sIx]);
	puts("</TH>");
	}
    else
	{
	printf("<TH align='center'>&nbsp;");
	printf("<A HREF=\'%s\' TARGET=BLANK>Restricted Until</A>", ENCODE_DATA_RELEASE_POLICY);
	puts("&nbsp;</TH>");
	}
    columnCount++;
    }
puts("</TR></THEAD>"); // The end of the header section.
settings->columnCount = columnCount;
}

static void printSubtrackTableFooter(int subCount, struct subtrackConfigSettings *settings)
/* Print footer with restriction policy if needed */
{
boolean restrictions = settings->restrictions;
sortOrder_t *sortOrder = settings->sortOrder;
int columnCount = settings->columnCount;

if (subCount > 5 || (restrictions && sortOrder != NULL))
    {
    printf("<TFOOT style='background-color:%s;'><TR valign='top'>", COLOR_BG_DEFAULT_DARKER);
    if (restrictions && sortOrder != NULL)
	printf("<TD colspan=%d>&nbsp;&nbsp;&nbsp;&nbsp;",columnCount-1);
    else
	printf("<TD colspan=%d>&nbsp;&nbsp;&nbsp;&nbsp;",columnCount);

    // Count of subtracks is filled in by javascript.
    if (subCount > 5)
	printf("<span class='subCBcount'></span>\n");

    printf("</TD>\n");
    // Restriction policy needs a link
    if (restrictions && sortOrder != NULL)
	printf("<TH><A HREF='%s' TARGET=BLANK style='font-size:.9em;'>Restriction Policy</A></TH>",
	       ENCODE_DATA_RELEASE_POLICY);

    printf("</TR></TFOOT>\n");
    }
}

/********************/
/* Basic info for a controlled vocabulary term */

struct vocabBasic {
struct vocabBasic *next;
char *term;
char *description;
char *url;
};

boolean vocabSettingIsEncode(char *setting)
/* Distinguish ENCODE controlled vocab settings (first arg is cv.ra filename) from non-ENCODE 
(table-based vocabs)
*/
{
if (setting && (strchr(cloneFirstWord(setting), '=') == NULL))
    return TRUE;
return FALSE;
}

char *vocabLink(struct hash *vocabFieldHash, char *term, char *title)
/* Make an anchor with mouseover containing description and link if present */
{   
struct vocabBasic *vocab = hashFindVal(vocabFieldHash, term);
if (vocab == NULL)
    return NULL;
struct dyString *ds = dyStringNew(0);
if (vocab->url == NULL || strlen(vocab->url) == 0)
    dyStringPrintf(ds, "<A title='%s' style='cursor: pointer;'>%s</A>",
		    vocab->description, term);
else
    dyStringPrintf(ds, "<A target='_blank' class='cv' title='%s' href='%s'>%s</A>\n",
		    vocab->description, vocab->url, term);
return dyStringCannibalize(&ds);
}

struct hash *vocabBasicFromSetting(struct trackDb *parentTdb, struct cart *cart)
/* Get description and URL for all vocabTables. Returns a hash of hashes */
{
char *spec = trackDbSetting(parentTdb, "controlledVocabulary");
if (!spec)
    return NULL;
// Not yet implemented for ENCODE-style CV
if (vocabSettingIsEncode(spec))
    return NULL;

struct slPair *vocabTables = slPairFromString(spec);
struct slPair *vocabTable = NULL;
struct hash *tableHash = hashNew(0);
struct sqlResult *sr;
char **row;
char query[256];
char *database = cartString(cart, "db");
for (vocabTable = vocabTables; vocabTable != NULL; vocabTable = vocabTable->next)
    {
    char *db = database;
    char *tableSpec = (char *)vocabTable->val;
    char *tableName = chopPrefix(tableSpec);
    if (differentString(tableName, tableSpec))
	{
	chopSuffix(tableSpec);
	db = tableSpec;
	}
    struct sqlConnection *conn = hAllocConn(db);
    boolean hasUrl = FALSE;
    struct hash *subgroupHash = hashNew(0);
    hashAdd(tableHash, vocabTable->name, subgroupHash);
    if (hHasField(db, tableName, "url"))
	{
	sqlSafef(query, sizeof(query), "select term, description, url from %s", tableName);
	hasUrl = TRUE;
	}
    else
	sqlSafef(query, sizeof(query), "select term, description from %s", tableName);
    sr = sqlGetResult(conn, query);
    while ((row = sqlNextRow(sr)) != NULL)
	{
	struct vocabBasic *vocab = NULL;
	AllocVar(vocab);
	vocab->term = cloneString(row[0]);
	vocab->description = cloneString(row[1]);
	if (hasUrl)
	    vocab->url = cloneString(row[2]);
	hashAdd(subgroupHash, vocab->term, vocab);
	}
    sqlFreeResult(&sr);
    hFreeConn(&conn);
    }
return tableHash;
}

static void printSubtrackTableBody(struct trackDb *parentTdb, struct slRef *subtrackRefList,
				struct subtrackConfigSettings *settings, struct cart *cart)
/* Print list of subtracks */
{
sortOrder_t *sortOrder = settings->sortOrder;
boolean useDragAndDrop = settings->useDragAndDrop;
boolean restrictions = settings->restrictions;
struct dyString *dyHtml = dyStringNew(SMALLBUF);
char buffer[SMALLBUF];
char id[SMALLBUF];
char *db = cartString(cart, "db");

// The subtracks need to be sorted by priority but only sortable and dragable will have
// non-default (cart) priorities to sort on
if (sortOrder != NULL || useDragAndDrop)
    {
    // preserves user's prev sort/drags, ignore returned value about where
    //  priorities come from
    (void) tdbRefSortPrioritiesFromCart(cart, &subtrackRefList);
    printf("<TBODY class='%saltColors'>\n", (sortOrder != NULL ? "sortable " : "") );
    }
else
    {
    slSort(&subtrackRefList, trackDbRefCmp);  // straight from trackDb.ra
    puts("<TBODY>");
    }

// Finally the big "for loop" to list each subtrack as a table row.
printf("\n<!-- ----- subtracks list ----- -->\n");
membersForAll_t* membersForAll = membersForAllSubGroupsGet(parentTdb,NULL);
struct hash *vocabHash = vocabBasicFromSetting(parentTdb, cart);
struct slRef *subtrackRef;

/* Color handling ?? */
//char *colors[2]   = { COLOR_BG_DEFAULT,
//                      COLOR_BG_ALTDEFAULT };
char *colors[2]   = { "bgLevel1",
		  "bgLevel1" };
int colorIx = settings->bgColorIx;

for (subtrackRef = subtrackRefList; subtrackRef != NULL; subtrackRef = subtrackRef->next)
    {
    struct trackDb *subtrack = subtrackRef->val;
    int ix;

    // Determine whether subtrack is checked, visible, configurable, has group membership, etc.
    int fourState = subtrackFourStateChecked(subtrack,cart);
    boolean checkedCB = fourStateChecked(fourState);
    boolean enabledCB = fourStateEnabled(fourState);
    boolean visibleCB = fourStateVisible(fourState);
    membership_t *membership = subgroupMembershipGet(subtrack);
    eCfgType cType = cfgNone;
    if (!tdbIsMultiTrack(parentTdb))  // MultiTracks never have configurable subtracks!
	cType = cfgTypeFromTdb(subtrack,FALSE);
    if (cType != cfgNone)
	{
	// Turn off configuring for certain track type or if explicitly turned off
	int cfgSubtrack = configurableByAjax(subtrack,cType);
	if (cfgSubtrack <= cfgNone)
	    cType = cfgNone;
	else if (membersForAll->members[dimV] && membership != NULL)
	    {  // subtrack only configurable if more than one subtrack in view
	       // find "view" in subgroup membership: e.g. "signal"
	    if (-1 != (ix = stringArrayIx(membersForAll->members[dimV]->groupTag,
					  membership->subgroups, membership->count)))
		{
		int ix2;                       // find "signal" in set of all views
		if (-1 != (ix2 = stringArrayIx(membership->membership[ix],
					       membersForAll->members[dimV]->tags,
					       membersForAll->members[dimV]->count)))
		    {
		    if (membersForAll->members[dimV]->subtrackCount[ix2] < 2)
			cType = cfgNone;
		    }
		}
	    }
	else if (slCount(subtrackRefList) < 2   // don't bother if there is a single subtrack
	     && cfgTypeFromTdb(parentTdb,FALSE) != cfgNone) // but the composite is configurable.
	    cType = cfgNone;
	}

    if (sortOrder == NULL && !useDragAndDrop)
	{
	char **lastDivide = NULL;
	dividers_t *dividers = dividersSettingGet(parentTdb);
	if (dividers)
	    lastDivide = needMem(sizeof(char*)*dividers->count);
	if (membership && divisionIfNeeded(lastDivide,dividers,membership) )
	    colorIx = (colorIx == COLOR_BG_DEFAULT_IX ? COLOR_BG_ALTDEFAULT_IX
						      : COLOR_BG_DEFAULT_IX);
	dividersFree(&dividers);
	}

    safef(id, sizeof(id), "%s_sel", subtrack->track);
    printf("<TR valign='top' class='%s%s'",
		colors[colorIx],(useDragAndDrop?" trDraggable":""));
    printf(" id=tr_%s%s>\n",id,(!visibleCB && !settings->displayAll?" style='display:none'":""));

    // Now the TD that holds the checkbox
    printf("<TD%s%s>",
	   (enabledCB?"":" title='view is hidden'"),
	   (useDragAndDrop?" class='dragHandle' title='Drag to reorder'":""));

    // A hidden field to keep track of subtrack order if it could change
    if (sortOrder != NULL || useDragAndDrop)
	{
	safef(buffer, sizeof(buffer), "%s.priority", subtrack->track);
	float priority = (float)cartUsualDouble(cart, buffer, subtrack->priority);
	printf("<INPUT TYPE=HIDDEN NAME='%s' class='trPos' VALUE=\"%.0f\">",
	       buffer, priority); // keeing track of priority
	}

    // The checkbox has identifying classes including subCB and the tag for each dimension
    //  (e.g. class='subCB GM12878 CTCF Peak')
    dyStringClear(dyHtml);
    dyStringAppend(dyHtml, "subCB"); // always first
    int di;
    if (membersForAll->dimensions && membership != NULL)
	{
	for (di=dimX;di<membersForAll->dimMax;di++)
	    {
	    if (membersForAll->members[di] && -1 !=
				(ix = stringArrayIx(membersForAll->members[di]->groupTag,
						    membership->subgroups, membership->count)))
		dyStringPrintf(dyHtml," %s",membership->membership[ix]);
	    }
	}
    else if (membersForAll->abcCount && membership != NULL) // "dimensions" don't exist but may be subgroups anyway
	{
	for (di=dimA;di<membersForAll->dimMax;di++)
	    {
	    if (membersForAll->members[di] && -1 !=
				(ix = stringArrayIx(membersForAll->members[di]->groupTag,
						    membership->subgroups, membership->count)))
		dyStringPrintf(dyHtml," %s",membership->membership[ix]);
	    }
	}
    if (membersForAll->members[dimV] && membership != NULL && -1 !=
				(ix = stringArrayIx(membersForAll->members[dimV]->groupTag,
						    membership->subgroups, membership->count)))
	dyStringPrintf(dyHtml, " %s",membership->membership[ix]);  // Saved view for last

    // And finally the checkBox is made!
    safef(buffer, sizeof(buffer), "%s_sel", subtrack->track);
    if (!enabledCB)
	{
	dyStringAppend(dyHtml, " disabled");
	cgiMakeCheckBoxFourWay(buffer,checkedCB,enabledCB,id,dyStringContents(dyHtml),
		"style='cursor:pointer' title='view is hidden'");
	jsOnEventById("click", id, "matSubCbClick(this);");
	}
    else
	{
	cgiMakeCheckBoxFourWay(buffer,checkedCB,enabledCB,id,dyStringContents(dyHtml),
			       "style='cursor:pointer'");
	jsOnEventById("click", id, "matSubCbClick(this);");
	}

    if (useDragAndDrop)
	printf("&nbsp;");

    if (!tdbIsMultiTrack(parentTdb))  // MultiTracks never have independent vis
	{
	printf("</TD><TD>"); // An extra column for subVis/wrench so dragAndDrop works
	enum trackVisibility vis = tdbVisLimitedByAncestors(cart,subtrack,FALSE,FALSE);
	char *view = NULL;
	if (membersForAll->members[dimV] && membership !=NULL
	&& -1 != (ix = stringArrayIx(membersForAll->members[dimV]->groupTag, membership->subgroups,
				     membership->count)))
	    view = membership->membership[ix];
	char classList[256];
	if (view != NULL)
	    safef(classList,sizeof(classList),"clickable fauxInput%s subVisDD %s",
			    (visibleCB ? "":" disabled"),view); // view should be last!
	else
	    safef(classList,sizeof(classList),"clickable fauxInput%s subVisDD",
			    (visibleCB ? "":" disabled"));
	#define SUBTRACK_CFG_VIS "<div id='%s_faux' class='%s' style='width:65px;'>%s</div>\n"
	printf(SUBTRACK_CFG_VIS,subtrack->track,classList,hStringFromTv(vis));
	char id[256];
	safef(id, sizeof id, "%s_faux", subtrack->track);
	jsOnEventByIdF("click", id, "return subCfg.replaceWithVis(this,\"%s\",true);", subtrack->track);
	
	if (cType != cfgNone)  // make a wrench
	    {
	    safef(id, sizeof id, "%s_toggle", subtrack->track);
	    #define SUBTRACK_CFG_WRENCH "<span id='%s' class='clickable%s' " \
					"title='Configure this subtrack'><img src='../images/wrench.png'>" \
                    "<span class='link'>&nbsp;Configure</span></span>\n"
	    printf(SUBTRACK_CFG_WRENCH,id, (visibleCB ? "":" disabled"));
	    jsOnEventByIdF("click", id, "return subCfg.cfgToggle(this,\"%s\");", subtrack->track);
	    }
	}
    printf("</TD>");

    // If sortable, then there must be a column per sortable dimension
    if (sortOrder != NULL)
        {
        int sIx=0;
        for (sIx=0; sIx <sortOrder->count; sIx++)
            {
            ix = -1;
            char *col = sortOrder->column[sIx];
            if (membership)
                ix = stringArrayIx(col, membership->subgroups, membership->count);
                // TODO: Sort needs to expand from subGroups to labels as well

            // only print the warning message for trackDb errors and not for the
            // default sortable columns of trackName and dateUnrestricted
            if ( (!membership || (membership && ix == -1) ) &&
                !(sameString(col, "trackName") || sameString(col, "dateUnrestricted") || sameString(col, "subtrackColor")) )
                {
                printf("<TD><span style=\"color:red\">Missing subgroup</span></TD>");
                }
            else
                {
                if (ix >= 0)
                    {
                    char *term = membership->membership[ix];
                    char *title = membership->titles[ix];
                    char *titleRoot=NULL;
                    if (cvTermIsEmpty(col, title))
                        titleRoot = cloneString(" &nbsp;");
                    else
                        titleRoot = labelRoot(title, NULL);
                    // Each sortable column requires hidden goop (in the "abbr" field currently)
                    // which is the actual sort on value
                    printf("<TD id='%s_%s' abbr='%s' align='left'>", subtrack->track, col, term);
                    printf("&nbsp;");
                    char *link = NULL;
                    if (vocabHash)
                        {
                        struct hash *colHash = hashFindVal(vocabHash, col);
                        if (colHash)
                        link = vocabLink(colHash, term, titleRoot);
                        }
                    printf("%s", link ? link : titleRoot);
                    puts("</TD>");
                    freeMem(titleRoot);
                    }
                else if (sameString(col, SUBTRACK_COLOR_SUBGROUP))
                    {
                    char *hue = subtrackColorToCompare(subtrack);
                    printf("<TD id='%s_%s' abbr='%s' bgcolor='#%02X%02X%02X'>"
                        "&nbsp;&nbsp;&nbsp;&nbsp;</TD>",
                        subtrack->track, col, hue, 
                        subtrack->colorR, subtrack->colorG, subtrack->colorB);
                    }
                }
            }
        }
    else  // Non-sortable tables do not have sort by columns but will display a short label
	{ // (which may be a configurable link)
	if (settings->colorPatch)
	    {
	    printf("<TD BGCOLOR='#%02X%02X%02X'>&nbsp;&nbsp;&nbsp;&nbsp;</TD>",
			   subtrack->colorR, subtrack->colorG, subtrack->colorB);

	    }
	printf("<TD>&nbsp;");
	hierarchy_t *hierarchy = hierarchySettingGet(parentTdb);
	indentIfNeeded(hierarchy,membership);
	hierarchyFree(&hierarchy);
	printf("%s",subtrack->shortLabel);
	puts("</TD>");
	}

    // The long label column (note that it may have a metadata dropdown)
    printf("<TD title='select to copy'>&nbsp;%s", subtrack->longLabel);
    if (trackDbSetting(parentTdb, "wgEncode") && trackDbSetting(subtrack, "accession"))
	printf(" [GEO:%s]", trackDbSetting(subtrack, "accession"));
    compositeMetadataToggle(db,subtrack,NULL,TRUE,FALSE);
    printf("&nbsp;");

    // Embedded cfg dialogs are within the TD that contains the longLabel.
    //  This allows a wide item to be embedded in the table
    if (cType != cfgNone)
	{
	// How to make this thing float to the left?  Container is overflow:visible
	// and contained (made in js) is position:relative; left: -{some pixels}
	#define CFG_SUBTRACK_DIV "<DIV id='div_cfg_%s' class='subCfg %s' style='display:none; " \
				 "overflow:visible;'></DIV>"
	#define MAKE_CFG_SUBTRACK_DIV(table,view) \
					printf(CFG_SUBTRACK_DIV,(table),(view)?(view):"noView")
	char * view = NULL;
	if (membersForAll->members[dimV] && membership != NULL && -1 !=
			    (ix = stringArrayIx(membersForAll->members[dimV]->groupTag,
						membership->subgroups, membership->count)))
	    view = membership->membership[ix];
	MAKE_CFG_SUBTRACK_DIV(subtrack->track,view);
	}

    // A schema link for each track
    printf("</td>\n<TD>&nbsp;");
    makeSchemaLink(db,subtrack,"Data format");
    printf("&nbsp;");

    // Do we have a restricted until date?
    if (restrictions)
	{
	char *dateDisplay = encodeRestrictionDate(db,subtrack,FALSE); // includes dates in the past
	if (dateDisplay)
	    {
	    if (dateIsOld(dateDisplay, MDB_ENCODE_DATE_FORMAT))
		printf("</TD>\n<TD align='center' nowrap style='color: #BBBBBB;'>&nbsp;%s&nbsp;",
		       dateDisplay);
	    else
		printf("</TD>\n<TD align='center'>&nbsp;%s&nbsp;", dateDisplay);
	    }
	}

    // End of row and free ourselves of this subtrack
    puts("</TD></TR>\n");

    boolean showCfg = trackDbSettingOn(subtrack, "showCfg");
    if (showCfg)
        jsInlineF(" subCfg.cfgToggle(document.getElementById(\"%s_toggle\"),\"%s\");",  subtrack->track, subtrack->track);

    }

// End of the table
puts("</TBODY>");
dyStringFree(&dyHtml);
membersForAllSubGroupsFree(parentTdb,&membersForAll);
}

static boolean membersHaveMatrix(membersForAll_t *membersForAll)
/* Check for matrix */
{
if (membersForAll->members[dimX] == NULL && membersForAll->members[dimY] == NULL)
    return FALSE;
return TRUE;
}


static void printSubtrackTable(struct trackDb *parentTdb, struct slRef *subtrackRefList,
			    struct subtrackConfigSettings *settings, struct cart *cart)
/* Print table of subtracks */
{
// Print table tag
printf("\n<TABLE CELLSPACING='2' CELLPADDING='0' border='0'");
struct dyString *dyHtml = dyStringNew(SMALLBUF);
if (settings->sortOrder != NULL)
    dyStringPrintf(dyHtml, "sortable");
if (settings->useDragAndDrop)
    {
    if (dyStringLen(dyHtml) > 0)
	dyStringAppendC(dyHtml,' ');
    dyStringPrintf(dyHtml, "tableWithDragAndDrop");
    }
printf(" class='subtracks");
if (dyStringLen(dyHtml) > 0)
    {
    printf(" bglevel1 %s'",dyStringContents(dyHtml));
    settings->bgColorIx = COLOR_BG_ALTDEFAULT_IX;
    }
else
    settings->bgColorIx = COLOR_BG_DEFAULT_IX; // Start with non-default allows alternation
puts("'>");
dyStringFree(&dyHtml);

// save count of subtracks for use by footer code
int subCount = slCount(subtrackRefList);

printSubtrackTableHeader(parentTdb, subtrackRefList, settings);
printSubtrackTableBody(parentTdb, subtrackRefList, settings, cart);
printSubtrackTableFooter(subCount, settings);
puts("</TABLE>");
}

boolean compositeHideEmptySubtracksSetting(struct trackDb *tdb, boolean *retDefault,
                                        char **retMultiBedFile, char **retSubtrackIdFile)
/* Parse hideEmptySubtracks settings
 * Format:  hideEmptySubtracks on|off
 *      Optional index files for performance:
 *          hideEmptySubtracksMultiBedUrl multiBed.bigBed 
 *          hideEmptySubtracksSourceUrl subtrackIds.tab
 * MultiBed.bed is a bed3Sources bigBed, generated with UCSC tool trackDbIndexBb
 *              (for single view subtracks, can use bedtools multiinter
 *              post-processed by UCSC multiBed.pl tool)
 *      subtrackIds.tab is a tab-sep file: id subtrackName
 *
 * Return TRUE if setting is present.  retDefault is TRUE if set to 'on', o/w FALSE
 */
{
if (!tdbIsComposite(tdb))
    return FALSE;
char *hideEmpties = cloneString(trackDbSetting(tdb, SUBTRACK_HIDE_EMPTY));
if (!hideEmpties)
    return FALSE;
boolean deflt = FALSE;
if (sameString(hideEmpties, "on"))
    deflt = TRUE;
else if (differentString(hideEmpties, "off"))
    {
    warn("Track %s %s setting invalid: %s", tdb->track, SUBTRACK_HIDE_EMPTY, hideEmpties);
    return FALSE;
    }
if (retDefault)
    *retDefault = deflt;
if (retMultiBedFile != NULL && retSubtrackIdFile != NULL)
    {
    char *file = cloneString(trackDbSetting(tdb, SUBTRACK_HIDE_EMPTY_MULTIBED_URL));
    if (file != NULL)
        {
        // multi-bed specified to speed display
        *retMultiBedFile = cloneString(hReplaceGbdb(file));
        file = cloneString(trackDbSetting(tdb, SUBTRACK_HIDE_EMPTY_SOURCES_URL));
        if (file == NULL)
            {
            warn("Track %s missing setting: %s", tdb->track, SUBTRACK_HIDE_EMPTY_SOURCES_URL);
            return FALSE;
            }
        *retSubtrackIdFile = cloneString(hReplaceGbdb(file));
        }
    }
return TRUE;
}

boolean compositeHideEmptySubtracks(struct cart *cart, struct trackDb *tdb,
                                        char **retMultiBedFile, char **retSubtrackIdFile)
/* Parse hideEmptySubtracks setting and check cart
 * Return TRUE if we should hide empties
 */
{
boolean deflt = FALSE;
if (!compositeHideEmptySubtracksSetting(tdb, &deflt, retMultiBedFile, retSubtrackIdFile))
    return FALSE;
char buf[128];
safef(buf, sizeof buf, "%s.%s", tdb->track, SUBTRACK_HIDE_EMPTY);
return cartUsualBoolean(cart, buf, deflt);
}

boolean compositeChildHideEmptySubtracks(struct cart *cart, struct trackDb *childTdb,
                                        char **retMultiBedFile, char **retSubtrackIdFile)
/* Parse hideEmptySubtracks setting and check cart
 * Return TRUE if we should hide empties
 */
{
struct trackDb *tdb = tdbGetComposite(childTdb);
if (!tdb)
    return FALSE;
return compositeHideEmptySubtracks(cart, tdb, retMultiBedFile, retSubtrackIdFile);

}

static void compositeUiSubtracks(char *db, struct cart *cart, struct trackDb *parentTdb)
// Display list of subtracks and descriptions with checkboxes to control visibility and
// possibly other nice things including links to schema and metadata and a release date.
{
char buffer[SMALLBUF];
struct trackDb *subtrack;

// Get list of leaf subtracks to work with
struct slRef *subtrackRef, *subtrackRefList = trackDbListGetRefsToDescendantLeaves(parentTdb->subtracks);

membersForAll_t* membersForAll = membersForAllSubGroupsGet(parentTdb,NULL);
sortOrder_t* sortOrder = sortOrderGet(cart, parentTdb);
char *displaySubs = NULL;
int subCount = slCount(subtrackRefList);
if (subCount > LARGE_COMPOSITE_CUTOFF && membersForAll->dimensions != NULL)
    {
    // ignore displaySubtracks setting for large composites with a matrix as
    // matrix effectively shows all
    safef(buffer, SMALLBUF,"%s.displaySubtracks", parentTdb->track);
    displaySubs = cartUsualString(cart, buffer,"some"); // track specific defaults to only selected
    }
else
    {
    displaySubs = cartUsualString(cart, "displaySubtracks", "all"); // browser wide defaults to all
    }
boolean displayAll = sameString(displaySubs, "all");

// Table wraps around entire list so that "Top" link can float to the correct place.
cgiDown(0.7);
printf("<table><tr><td class='windowSize'>");
printf("<A NAME='DISPLAY_SUBTRACKS'></A>");
if (sortOrder != NULL)
    {
    // First table row contains the display "selected/visible" or "all" radio buttons
    // NOTE: list subtrack radio buttons are inside tracklist table header if
    //       there are no sort columns.  The reason is to ensure spacing of lines
    //       column headers when the only column header is "Restricted Until"
    printSubtrackListRadioButtons(parentTdb->track, subCount, displayAll);
    if (membersHaveMatrix(membersForAll))
	makeTopLink(parentTdb);
    printf("</td></tr></table>");
    }
else
    {
    if (membersHaveMatrix(membersForAll))
	makeTopLink(parentTdb);
    }

// Get info for subtrack list
struct subtrackConfigSettings *settings = NULL;
AllocVar(settings);

// Determine whether there is a restricted until date column
settings->restrictions = FALSE;
for (subtrackRef = subtrackRefList; subtrackRef != NULL; subtrackRef = subtrackRef->next)
    {
    subtrack = subtrackRef->val;
    (void)metadataForTable(db,subtrack,NULL);
    if (NULL != metadataFindValue(subtrack,"dateUnrestricted"))
	{
	settings->restrictions = TRUE;
	break;
	}
    }
settings->useDragAndDrop = sameOk("subTracks",trackDbSetting(parentTdb, "dragAndDrop"));
settings->sortOrder = sortOrder;
settings->displayAll = displayAll;
settings->colorPatch = (trackDbSetting(parentTdb, SUBTRACK_COLOR_PATCH) != NULL);

printSubtrackTable(parentTdb, subtrackRefList, settings, cart);

if (sortOrder == NULL)
    printf("</td></tr></table>");

membersForAllSubGroupsFree(parentTdb,&membersForAll);
sortOrderFree(&sortOrder);
}

static void compositeUiSubtracksMatchingPrimary(char *db, struct cart *cart,
					    struct trackDb *parentTdb,char *primarySubtrack)
// Display list of subtracks associated with a primary subtrack for the hgTables merge function
{
assert(primarySubtrack != NULL);
char *primaryType = getPrimaryType(primarySubtrack, parentTdb);
char htmlIdentifier[SMALLBUF];

// Get list of leaf subtracks to work with and sort them
struct slRef *subtrackRef, *subtrackRefList =
			    trackDbListGetRefsToDescendantLeaves(parentTdb->subtracks);
if (NULL != trackDbSetting(parentTdb, "sortOrder")
||  NULL != trackDbSetting(parentTdb, "dragAndDrop"))
    tdbRefSortPrioritiesFromCart(cart, &subtrackRefList); // preserves user's prev sort/drags
else
    slSort(&subtrackRefList, trackDbRefCmp);  // straight from trackDb.ra

// Now we can start in on the table of subtracks
printf("\n<TABLE CELLSPACING='2' CELLPADDING='0' border='0' id='subtracks.%s'>"
   "<THEAD>\n</TR></THEAD><TBODY>\n",parentTdb->track);

for (subtrackRef = subtrackRefList; subtrackRef != NULL; subtrackRef = subtrackRef->next)
    {
    struct trackDb *subtrack = subtrackRef->val;
    int fourState = subtrackFourStateChecked(subtrack,cart);
    boolean checkedCB = fourStateChecked(fourState);
    boolean enabledCB = fourStateEnabled(fourState);
    safef(htmlIdentifier, sizeof(htmlIdentifier), "%s_sel", subtrack->track);

    if (sameString(subtrack->track, primarySubtrack))
	{
	puts("<TR><TD>");
	cgiMakeHiddenBoolean(htmlIdentifier, TRUE);
	puts("[on] ");
	printf("</TD><TD>%s [selected on main page]</TD></TR>\n",
	       subtrack->longLabel);
	}
    else if (hSameTrackDbType(primaryType, subtrack->type))
	{
	puts("<TR><TD>");
	cgiMakeCheckBox(htmlIdentifier, checkedCB && enabledCB);
	printf("</TD><TD>%s</TD></TR>\n", subtrack->longLabel);
	}
    }
puts("</TBODY><TFOOT></TFOOT>");
puts("</TABLE>");
if (slCount(subtrackRefList) > 5)
    puts("&nbsp;&nbsp;&nbsp;&nbsp;<span class='subCBcount'></span>");
puts("<P>");
if (!primarySubtrack)
    jsInline("matInitializeMatrix();\n");
}

static void makeAddClearButtonPair(char *idPrefix, char *class,char *separator)
// Print an [Add][Clear] button pair that uses javascript to check subtracks
{
char buf[256];
if (class)
    safef(buf, sizeof buf,"matSetMatrixCheckBoxes(true,'%s'); return false;", class);
else
    safef(buf, sizeof buf,"matSetMatrixCheckBoxes(true); return false;");
char id[256];
safef(id, sizeof id, "%s_add", idPrefix);
cgiMakeOnClickButton(id, buf, ADD_BUTTON_LABEL);
if (separator)
    printf("%s",separator);
if (class)
    safef(buf, sizeof buf,"matSetMatrixCheckBoxes(false,'%s'); return false;", class);
else
    safef(buf, sizeof buf,"matSetMatrixCheckBoxes(false); return false;");
safef(id, sizeof id, "%s_clr", idPrefix);
cgiMakeOnClickButton(id, buf, CLEAR_BUTTON_LABEL);
}

#define MANY_SUBTRACKS  8
#define WIGGLE_HELP_PAGE  "../goldenPath/help/hgWiggleTrackHelp.html"

boolean cfgBeginBoxAndTitle(struct trackDb *tdb, boolean boxed, char *title)
// Handle start of box and title for individual track type settings
{
if (!boxed)
    {
    boxed = trackDbSettingOn(tdb,"boxedCfg");
    if (boxed)
        printf("<BR>");
    }
if (boxed)
    {
    printf("<TABLE class='blueBox");
    char *view = tdbGetViewName(tdb);
    if (view != NULL)
        printf(" %s",view);
    printf("' style='background-color:%s;'><TR><TD>", COLOR_BG_ALTDEFAULT);
    if (title)
        printf("<CENTER><B>%s Configuration</B></CENTER>\n", title);
    }
else if (title)
    printf("<p><B>%s &nbsp;</b>", title );
else
    printf("<p>");
return boxed;
}

void cfgEndBox(boolean boxed)
// Handle end of box and title for individual track type settings
{
if (boxed)
    puts("</td></tr></table>");
}

void snakeOption(struct cart *cart, char *name, char *title, struct trackDb *tdb)
/* let the user choose to see the track in snake mode */
{
if (!cfgOptionBooleanDefault("canSnake", FALSE))
    return;

printf("<BR><B>Display data as a rearrangement graph:</B> ");
boolean option = cartOrTdbBoolean(cart, tdb, "doSnake", FALSE);

char varName[1024];
safef(varName, sizeof(varName), "%s.doSnake", name);
cgiMakeCheckBox(varName, option);
printf("<BR>\n");

//char *style = option ? "display:block" : "display:none";
//printf("<DIV ID=\"snakeGraphOptions\" STYLE=\"%s\">\n", style);
//printf("</DIV>\n\n");

jsInlineF("$(\"input[name='%s']\").click( function() { $('#snakeGraphOptions').toggle();} );\n"
    , varName); // XSS FILTER?
}

void squishyPackOption(struct cart *cart, char *name, char *title, struct trackDb *tdb)
/* let the user choose to see the track in wiggle mode */
{
char option[256];
char buffer[4096];
char *field = trackDbSetting(tdb, "squishyPackField");
if (field == NULL)
    return;
char *fieldLabel = trackDbSetting(tdb, "squishyPackLabel");
if (fieldLabel == NULL)
    {
    fieldLabel = buffer;
    safef(buffer, sizeof buffer, "Reduce (squish) the height of items that have a %s value greater than", field);
    }

double squishyPackPoint = cartOrTdbDouble(cart, tdb, "squishyPackPoint", 999);
printf("<BR><B>%s</B> ", fieldLabel);

safef(option, sizeof(option), "%s.%s", name, "squishyPackPoint" );
cgiMakeDoubleVarWithLimits(option, squishyPackPoint, "Range min", 0, NO_VALUE, NO_VALUE);
}

void wigOption(struct cart *cart, char *name, char *title, struct trackDb *tdb)
/* let the user choose to see the track in wiggle mode */
{
printf("<BR><BR><B>Display data as a density graph:</B> ");
boolean option = cartOrTdbBoolean(cart, tdb, "doWiggle", FALSE);

char varName[1024];
safef(varName, sizeof(varName), "%s.doWiggle", name);
cgiMakeCheckBox(varName, option);
printf("<BR>\n");
char *style = option ? "display:block" : "display:none";
printf("<DIV ID=\"densGraphOptions\" STYLE=\"%s\">\n", style);

// we need to fool the wiggle dialog into defaulting to autoscale and maximum
char *origType = tdb->type;
tdb->type = "bedGraph";
if (hashFindVal(tdb->settingsHash, AUTOSCALE) == NULL)
    hashAdd(tdb->settingsHash, AUTOSCALE, "on");
if (hashFindVal(tdb->settingsHash, WINDOWINGFUNCTION) == NULL)
    hashAdd(tdb->settingsHash, WINDOWINGFUNCTION, wiggleWindowingEnumToString( wiggleWindowingMean));
wigCfgUi(cart,tdb,name,title,TRUE);
tdb->type = origType;
printf("</DIV>\n\n");
jsInlineF("$(\"input[name='%s']\").click( function() { $('#densGraphOptions').toggle();} );\n"
    , varName); // XSS FILTER?
}

void wiggleScaleDropDownJavascript(char *name)
/* print some js that deactivates the min/max range if autoscaling is activated */
{
struct dyString *dy = dyStringNew(1024);
dyStringPrintf(dy, "  $(\"[name='%s.autoScale']\").change(function()\n", name);
dyStringPrintf(dy, "  {\n");
dyStringPrintf(dy, "  val= $(this).find(':selected').val(); \n");
dyStringPrintf(dy, "  if (val!=\"use vertical viewing range setting\")\n");
dyStringPrintf(dy, "     {\n");
dyStringPrintf(dy, "     $(\"[name='%s.minY']\")[0].disabled=true;\n", name);
dyStringPrintf(dy, "     $(\"[name='%s.maxY']\")[0].disabled=true;\n", name);
dyStringPrintf(dy, "     $(\".%sAutoScaleDesc\").attr('style', 'color:grey;');\n", name);
dyStringPrintf(dy, "     }\n");
dyStringPrintf(dy, "     else\n");
dyStringPrintf(dy, "     {\n");
dyStringPrintf(dy, "     $(\"[name='%s.minY']\")[0].disabled=false;\n", name);
dyStringPrintf(dy, "     $(\"[name='%s.maxY']\")[0].disabled=false;\n", name);
dyStringPrintf(dy, "     $(\".%sAutoScaleDesc\").attr('style', 'color:black;');\n", name);
dyStringPrintf(dy, "     }\n");
dyStringPrintf(dy, "  });\n");
dyStringPrintf(dy, "\n");
dyStringPrintf(dy, "  $( document ).ready(function()\n");
dyStringPrintf(dy, "  {\n");
dyStringPrintf(dy, "  val= $(\"[name='%s.autoScale']\").find(':selected').val(); \n", name);
dyStringPrintf(dy, "  if (val==\"auto-scale to data view\")\n");
dyStringPrintf(dy, "     {\n");
dyStringPrintf(dy, "     $(\"[name='%s.minY']\")[0].disabled=true;\n", name);
dyStringPrintf(dy, "     $(\"[name='%s.maxY']\")[0].disabled=true;\n", name);
dyStringPrintf(dy, "     $(\".%sAutoScaleDesc\").attr('style', 'color:grey;');\n", name);
dyStringPrintf(dy, "     }\n");
dyStringPrintf(dy, "  });\n");
jsInline(dy->string);
dyStringFree(&dy);
}

void wigCfgUi(struct cart *cart, struct trackDb *tdb, char *name, char *title, boolean boxed)
/* UI for the wiggle track */
{
char *typeLine = NULL;  /*  to parse the trackDb type line  */
char *words[8];     /*  to parse the trackDb type line  */
int wordCount = 0;  /*  to parse the trackDb type line  */
char option[256];
double minY;        /*  from trackDb or cart    */
double maxY;        /*  from trackDb or cart    */
double tDbMinY;     /*  data range limits from trackDb type line */
double tDbMaxY;     /*  data range limits from trackDb type line */
char *horizontalGrid = NULL;    /*  Grid lines, off by default */
char *transformFunc = NULL;    /* function to transform data points */
char *alwaysZero = NULL;    /* Always include 0 in range */
char *lineBar;  /*  Line or Bar graph */
char *autoScale;    /*  Auto scaling on or off */
char *windowingFunction;    /*  Maximum, Mean, or Minimum */
char *smoothingWindow;  /*  OFF or [2:16] */
char *yLineMarkOnOff;   /*  user defined Y marker line to draw */
double yLineMark;       /*  from trackDb or cart    */
int maxHeightPixels = atoi(DEFAULT_HEIGHT_PER);
int minHeightPixels = MIN_HEIGHT_PER;
int defaultHeight = maxHeightPixels;  /*  pixels per item */

boxed = cfgBeginBoxAndTitle(tdb, boxed, title);

wigFetchMinMaxPixelsWithCart(cart,tdb,name,&minHeightPixels, &maxHeightPixels, &defaultHeight);
typeLine = cloneString(tdb->type);
wordCount = chopLine(typeLine,words);

wigFetchMinMaxYWithCart(cart, tdb, name, &minY, &maxY, &tDbMinY, &tDbMaxY, wordCount, words);
freeMem(typeLine);

wigFetchTransformFuncWithCart(cart,tdb,name, &transformFunc);
wigFetchAlwaysZeroWithCart(cart,tdb,name, &alwaysZero);
wigFetchHorizontalGridWithCart(cart,tdb,name, &horizontalGrid);
wigFetchAutoScaleWithCart(cart,tdb,name, &autoScale);
wigFetchGraphTypeWithCart(cart,tdb,name, &lineBar);
wigFetchWindowingFunctionWithCart(cart,tdb,name, &windowingFunction);
wigFetchSmoothingWindowWithCart(cart,tdb,name, &smoothingWindow);
wigFetchYLineMarkWithCart(cart,tdb,name, &yLineMarkOnOff);
wigFetchYLineMarkValueWithCart(cart,tdb,name, &yLineMark);
boolean doNegative = wigFetchDoNegativeWithCart(cart,tdb,tdb->track, (char **) NULL);
boolean doSequenceLogo = wigFetchDoSequenceLogoWithCart(cart,tdb,tdb->track, (char **) NULL);

printf("<TABLE BORDER=0>");

boolean isLogo = ((tdb->parent != NULL) && trackDbSetting(tdb->parent, "logo") != NULL);
boolean parentLevel = isNameAtParentLevel(tdb, name);
boolean didAggregate = FALSE;
if (parentLevel && !isLogo)
    {
    assert(tdb->parent != NULL);
    char *aggregate = trackDbSetting(tdb->parent, "aggregate");
    if (aggregate != NULL && parentLevel)
        {
        char *aggregateVal = cartOrTdbString(cart, tdb->parent, "aggregate", NULL);
        safef(option, sizeof(option), "%s.%s", name, AGGREGATE);
        if (isCustomComposite(tdb))
            {
            printf("<TR valign=middle><th align=right>Merge method:</th><td align=left>");
            aggregateExtraDropDown(option, aggregateVal);
            }
        else
            {
            printf("<TR valign=middle><th align=right>Overlay method:</th><td align=left>");
            aggregateDropDown(option, aggregateVal);
            }
        puts("</td></TR>");

	if (sameString(aggregateVal, WIG_AGGREGATE_STACKED)  &&
	    sameString(windowingFunction, "mean+whiskers"))
	    {
	    windowingFunction = "maximum";
	    }

	didAggregate = TRUE;
        }
    if (isCustomComposite(tdb))
        {
        /*
        char *viewFuncVal = cartOrTdbString(cart, tdb->parent, "viewFunc", NULL);
        printf("<TR valign=middle><th align=right>Math method:</th><td align=left>");
        safef(option, sizeof(option), "%s.%s", name, VIEWFUNC);
        viewFuncDropDown(option, viewFuncVal);
        */

        printf("<TR valign=middle><th align=right>Missing data treatment:</th><td align=left>");
        char *missingMethodVal = cartOrTdbString(cart, tdb->parent, "missingMethod", NULL);
        boolean missingIsZero = (missingMethodVal == NULL) ||  differentString(missingMethodVal, "missing");
        char buffer[1024];
        safef(buffer, sizeof buffer, "%s.missingMethod",name);

        cgiMakeOnEventRadioButtonWithClass(buffer, "zero", missingIsZero, "allOrOnly", "click", NULL);
        puts("missing is zero&nbsp;&nbsp;");
        cgiMakeOnEventRadioButtonWithClass(buffer, "missing", !missingIsZero, "allOrOnly", "click", NULL);
        printf("math with missing values is missing</B>");
        }
    }

if (!isLogo)
    {
    printf("<TR valign=middle><th align=right>Type of graph:</th><td align=left>");
    safef( option, sizeof(option), "%s.%s", name, LINEBAR );
    wiggleGraphDropDown(option, lineBar);
    if (boxed)
        {
        printf("</td><td align=right colspan=2>");
        printf("<A HREF=\"%s\" TARGET=_blank>Graph configuration help</A>",WIGGLE_HELP_PAGE);
        }
    puts("</td></TR>");
    }

printf("<TR valign=middle><th align=right>Track height:</th><td align=left colspan=3>");
safef(option, sizeof(option), "%s.%s", name, HEIGHTPER );
cgiMakeIntVarWithLimits(option, defaultHeight, "Track height",0, minHeightPixels, maxHeightPixels);
printf("pixels&nbsp;(range: %d to %d)",
       minHeightPixels, maxHeightPixels);
puts("</TD></TR>");

printf("<TR valign=middle><th align=right>Data view scaling:</th><td align=left colspan=3>");
safef(option, sizeof(option), "%s.%s", name, AUTOSCALE );
if (tdb->parent || tdb->subtracks)
    wiggleScaleDropDownParent(option, autoScale);
else
    wiggleScaleDropDown(option, autoScale);
wiggleScaleDropDownJavascript(name);

if (!isLogo)
    {
    safef(option, sizeof(option), "%s.%s", name, ALWAYSZERO);
    printf("Always include zero:&nbsp;");
    wiggleAlwaysZeroDropDown(option, alwaysZero);
    puts("</TD></TR>");
    }

printf("<TR class=\"%sAutoScaleDesc\" valign=middle><th align=right>Vertical viewing range:</th>"
       "<td align=left>&nbsp;min:&nbsp;", name);
safef(option, sizeof(option), "%s.%s", name, MIN_Y );
cgiMakeDoubleVarWithLimits(option, minY, "Range min", 0, NO_VALUE, NO_VALUE);
printf("</td><td align=leftv colspan=2>max:&nbsp;");
safef(option, sizeof(option), "%s.%s", name, MAX_Y );
cgiMakeDoubleVarWithLimits(option, maxY, "Range max", 0, NO_VALUE, NO_VALUE);
if (!isCustomComposite(tdb))
    printf("&nbsp;(range: %g to %g)",
           tDbMinY, tDbMaxY);
puts("</TD></TR>");

printf("<TR valign=middle><th align=right>Transform function:</th><td align=left>");
safef(option, sizeof(option), "%s.%s", name, TRANSFORMFUNC);
printf("Transform data points by:&nbsp;");
wiggleTransformFuncDropDown(option, transformFunc);
puts("</TD></TR>");

if (!isLogo)
    {
    printf("<TR valign=middle><th align=right>Windowing function:</th><td align=left>");
    safef(option, sizeof(option), "%s.%s", name, WINDOWINGFUNCTION );
    wiggleWindowingDropDown(option, windowingFunction);
    puts("</TD>");

    printf("<th align=right>Smoothing window:</th><td align=left>");
    safef(option, sizeof(option), "%s.%s", name, SMOOTHINGWINDOW );
    wiggleSmoothingDropDown(option, smoothingWindow);
    puts("&nbsp;pixels</TD></TR>");

    printf("<TR valign=middle>");
    printf("<th align=right>Negate values:</th><td align=left>");
    safef(option, sizeof(option), "%s.%s", name, DONEGATIVEMODE );
    cgiMakeCheckBox(option, doNegative);
    puts("</TD></TR>");

    printf("<TR valign=middle><td align=right><b>Draw y indicator lines:</b></td>"
           "<td align=left colspan=2>");
    printf("at y = 0.0:");
    safef(option, sizeof(option), "%s.%s", name, HORIZGRID );
    wiggleGridDropDown(option, horizontalGrid);
    printf("&nbsp;&nbsp;&nbsp;at y =");
    safef(option, sizeof(option), "%s.%s", name, YLINEMARK );
    cgiMakeDoubleVarInRange(option, yLineMark, "Indicator at Y", 0, NULL, NULL);
    safef(option, sizeof(option), "%s.%s", name, YLINEONOFF );
    wiggleYLineMarkDropDown(option, yLineMarkOnOff);

    char *logoMaf = trackDbSetting(tdb, "logoMaf");

    if (logoMaf)
        {
        printf("<TR valign=middle><td align=right><b>Draw sequence logo when near base level:</b></td>"
               "<td align=left colspan=2>");
        safef(option, sizeof(option), "%s.%s", name, DOSEQUENCELOGOMODE );
        cgiMakeCheckBox(option, doSequenceLogo);
        }
    }
if (boxed)
    puts("</TD></TR></TABLE>");
else
    {
    puts("</TD></TR></TABLE>");
    if (!isLogo)
        printf("<A HREF=\"%s\" TARGET=_blank>Graph configuration help</A>",WIGGLE_HELP_PAGE);
    }

// add a little javascript call to make sure we don't get whiskers with stacks in multiwigs

if (didAggregate)
    jsInlineF("$(function () { multiWigSetupOnChange('%s'); });\n", name);

cfgEndBox(boxed);
}


void filterButtons(char *filterTypeVar, char *filterTypeVal, boolean none)
/* Put up some filter buttons. */
{
printf("<B>Filter:</B> ");
radioButton(filterTypeVar, filterTypeVal, "red");
radioButton(filterTypeVar, filterTypeVal, "green");
radioButton(filterTypeVar, filterTypeVal, "blue");
radioButton(filterTypeVar, filterTypeVal, "exclude");
radioButton(filterTypeVar, filterTypeVal, "include");
if (none)
    radioButton(filterTypeVar, filterTypeVal, "none");
}

void radioButton(char *var, char *val, char *ourVal)
/* Print one radio button */
{
cgiMakeRadioButton(var, ourVal, sameString(ourVal, val));
printf("%s ", ourVal);
}

void oneMrnaFilterUi(struct controlGrid *cg, struct trackDb *tdb, char *text, char *var,
                     char *suffix, struct cart *cart)
/* Print out user interface for one type of mrna filter. */
{
controlGridStartCell(cg);
printf("%s:<BR>", text);
boolean parentLevel = isNameAtParentLevel(tdb,var);
cgiMakeTextVar(var, cartUsualStringClosestToHome(cart, tdb, parentLevel,suffix, ""), 19);
controlGridEndCell(cg);
}

void bedFiltCfgUi(struct cart *cart, struct trackDb *tdb, char *prefix, char *title, boolean boxed)
/* Put up UI for an "bedFilter" tracks. */
{
struct mrnaUiData *mud = newBedUiData(prefix);
struct mrnaFilter *fil;
struct controlGrid *cg = NULL;
boolean parentLevel = isNameAtParentLevel(tdb,prefix);
char *filterTypeVal =
                cartUsualStringClosestToHome(cart, tdb, parentLevel, mud->filterTypeSuffix, "red");
boxed = cfgBeginBoxAndTitle(tdb, boxed, title);
/* Define type of filter. */
printf("<table width=400><tr><td align='left'>\n");
char buffer[256];
safef(buffer, sizeof buffer,"%s.%s",prefix,mud->filterTypeSuffix);
filterButtons(buffer, filterTypeVal, FALSE);
printf("</br>");
/* List various fields you can filter on. */
cg = startControlGrid(4, NULL);
for (fil = mud->filterList; fil != NULL; fil = fil->next)
    {
    safef(buffer, sizeof buffer,"%s.%s",prefix,fil->suffix);
    oneMrnaFilterUi(cg, tdb, fil->label, buffer, fil->suffix, cart);
    }
endControlGrid(&cg);
cfgEndBox(boxed);
}

void genbankShowPatentControl(struct cart *cart, struct trackDb *tdb, char *prefix)
/* controls for enabling display of GENBANK RNA patent sequences */
{
char name[256];
safef(name, sizeof(name), "%s.%s", prefix, SHOW_PATENT_SEQUENCES_SUFFIX);
printf("<P><B>Show patent sequences</B>:");
cgiMakeCheckBox(name, cartUsualBoolean(cart, name, FALSE));
}

void mrnaCfgUi(struct cart *cart, struct trackDb *tdb, char *prefix, char *title, boolean boxed)
/* Put up UI for an mRNA (or EST) track. */
{
boolean isXeno = (sameString(tdb->track, "xenoMrna") ||  sameString(tdb->track, "xenoEst"));
struct mrnaUiData *mud = newMrnaUiData(prefix, isXeno);
struct mrnaFilter *fil;
struct controlGrid *cg = NULL;
boolean parentLevel = isNameAtParentLevel(tdb,prefix);
char *filterTypeVal =
                cartUsualStringClosestToHome(cart, tdb, parentLevel, mud->filterTypeSuffix,"red");
char *logicTypeVal  =
                cartUsualStringClosestToHome(cart, tdb, parentLevel, mud->logicTypeSuffix, "and");

boxed = cfgBeginBoxAndTitle(tdb, boxed, title);
/* Define type of filter. */
char buffer[256];
safef(buffer,sizeof buffer,"%s.%s",prefix,mud->filterTypeSuffix);
filterButtons(buffer, filterTypeVal, FALSE);
printf("  <B>Combination Logic:</B> ");
safef(buffer,sizeof buffer,"%s.%s",prefix,mud->logicTypeSuffix);
radioButton(buffer, logicTypeVal, "and");
radioButton(buffer, logicTypeVal, "or");
printf("<BR>\n");

/* List various fields you can filter on. */
printf("<table border=0 cellspacing=1 cellpadding=1 width=940>\n");
cg = startControlGrid(4, NULL);
for (fil = mud->filterList; fil != NULL; fil = fil->next)
    {
    safef(buffer,sizeof buffer,"%s.%s",prefix,fil->suffix);
    oneMrnaFilterUi(cg, tdb, fil->label, buffer, fil->suffix, cart);
    }
endControlGrid(&cg);
baseColorDrawOptDropDown(cart, tdb);
indelShowOptions(cart, tdb);
if (sameString(tdb->track, "mrna") || sameString(tdb->track, "xenoMrna"))
    genbankShowPatentControl(cart, tdb, prefix);
wigOption(cart, prefix, title, tdb);
cfgEndBox(boxed);
}


void scoreGrayLevelCfgUi(struct cart *cart, struct trackDb *tdb, char *prefix, int scoreMax)
// If scoreMin has been set, let user select the shade of gray for that score, in case
// the default is too light to see or darker than necessary.
{
boolean parentLevel = isNameAtParentLevel(tdb,prefix);
char *scoreMinStr = trackDbSettingClosestToHome(tdb, GRAY_LEVEL_SCORE_MIN);
if (scoreMinStr != NULL)
    {
    int scoreMin = atoi(scoreMinStr);
    // maxShade=9 taken from hgTracks/simpleTracks.c.  Ignore the 10 in shadesOfGray[10+1] --
    // maxShade is used to access the array.
    int maxShade = 9;
    int scoreMinGrayLevel = scoreMin * maxShade/scoreMax;
    if (scoreMinGrayLevel <= 0) scoreMinGrayLevel = 1;
    char *setting = trackDbSettingClosestToHome(tdb, MIN_GRAY_LEVEL);
    int minGrayLevel = cartUsualIntClosestToHome(cart, tdb, parentLevel, MIN_GRAY_LEVEL,
                        setting ? atoi(setting) : scoreMinGrayLevel);
    if (minGrayLevel <= 0) minGrayLevel = 1;
    if (minGrayLevel > maxShade) minGrayLevel = maxShade;
    puts("\n<B>Shade of lowest-scoring items: </B>");
    // Add javascript to select so that its color is consistent with option colors:
    int level = 255 - (255*minGrayLevel / maxShade);
    printf("<SELECT NAME=\"%s.%s\" STYLE='color: #%02x%02x%02x' class='normalText'",
           prefix, MIN_GRAY_LEVEL, level, level, level);
    int i;
    puts(">\n");
    // Use class to set color of each option:
    for (i = 1;  i <= maxShade;  i++)
        {
        level = 255 - (255*i / maxShade);
        printf("<OPTION%s STYLE='color: #%02x%02x%02x' VALUE=%d>",
               (minGrayLevel == i ? " SELECTED" : ""), level, level, level, i);
        if (i == maxShade)
            printf("&bull; black</OPTION>\n");
        else
            printf("&bull; gray (%d%%)</OPTION>\n", i * (100/maxShade));
        }
    printf("</SELECT>\n");
    }
}

static boolean getScoreDefaultsFromTdb(struct trackDb *tdb, char *scoreName,char *defaults,
                                       char**min,char**max)
// returns TRUE if defaults exist and sets the string pointer (because they may be float or int)
// if min or max are set, then they should be freed
{
if (min)
    *min = NULL; // default these outs!
if (max)
    *max = NULL;
char *setting = trackDbSettingClosestToHome(tdb, scoreName);
if (setting)
    {
    if (strchr(setting,':') != NULL)
        return colonPairToStrings(setting,min,max);
    else if (min)
        *min = cloneString(setting);
    return TRUE;
    }
return FALSE;
}

static void setAsNewFilterType(struct trackDb *tdb, char *name, char *field)
/* put the full name of the trackDb variable in a hash of field names if it's specified in the "new" way */
{
struct hash *hash = tdb->isNewFilterHash;

if (hash == NULL)
    hash = tdb->isNewFilterHash = newHash(5);

hashAdd(hash, field, name);
}

static char *isNewFilterType(struct trackDb *tdb, char *name)
/* check to see if a field name is in the "new" hash.  If it is, return the full trackDb variable name */
{
if ((tdb == NULL) || (tdb->isNewFilterHash == NULL))
    return NULL;

struct hashEl *hel = hashLookup(tdb->isNewFilterHash, name);

if (hel == NULL)
    return NULL;

return hel->val;
}

char *getScoreNameAdd(struct trackDb *tdb, char *scoreName, char *add)
// Add a suffix to a filter for more information
{
char scoreLimitName[1024];
char *name = cloneString(scoreName);
char *dot = strchr(name, '.');

if ((dot != NULL) && (isNewFilterType(tdb, dot+1) != NULL))
    {
    *dot++ = 0;
    safef(scoreLimitName, sizeof(scoreLimitName), "%s%s.%s", name, add, dot);
    }
else
    safef(scoreLimitName, sizeof(scoreLimitName), "%s%s", scoreName, add);
return cloneString(scoreLimitName);
}

static boolean getScoreLimitsFromTdb(struct trackDb *tdb, char *scoreName,char *defaults,
                                     char**min,char**max)
// returns TRUE if limits exist and sets the string pointer (because they may be float or int)
// if min or max are set, then they should be freed
{
if (min)
    *min = NULL; // default these outs!
if (max)
    *max = NULL;

char *scoreLimitName = getScoreNameAdd(tdb, scoreName, _LIMITS);
char *setting = trackDbSettingClosestToHome(tdb, scoreLimitName);
if (setting)
    {
    return colonPairToStrings(setting,min,max);
    }
else
    {
    if (min)
        {
        scoreLimitName = getScoreNameAdd(tdb, scoreName, _MIN);
        setting = trackDbSettingClosestToHome(tdb, scoreLimitName);
        if (setting)
            *min = cloneString(setting);
        }
    if (max)
        {
        scoreLimitName = getScoreNameAdd(tdb, scoreName, _MAX);
        setting = trackDbSettingClosestToHome(tdb, scoreLimitName);
        if (setting)
            *max = cloneString(setting);
        }
    return TRUE;
    }
if (defaults != NULL && ((min && *min == NULL) || (max && *max == NULL)))
    {
    char *minLoc=NULL;
    char *maxLoc=NULL;
    if (colonPairToStrings(defaults,&minLoc,&maxLoc))
        {
        if (min && *min == NULL && minLoc != NULL)
            *min=minLoc;
        else
            freeMem(minLoc);
        if (max && *max == NULL && maxLoc != NULL)
            *max=maxLoc;
        else
            freeMem(maxLoc);
        return TRUE;
        }
    }
return FALSE;
}

void getScoreIntRangeFromCart(struct cart *cart, struct trackDb *tdb, boolean parentLevel,
                                 char *scoreName, int *limitMin, int *limitMax,int *min,int *max)
// gets an integer score range from the cart, but the limits from trackDb
// for any of the pointers provided, will return a value found, if found, else it's contents
// are undisturbed (use NO_VALUE to recognize unavaliable values)
{
char scoreLimitName[128];
char *deMin=NULL,*deMax=NULL;
if ((limitMin || limitMax) && getScoreLimitsFromTdb(tdb,scoreName,NULL,&deMin,&deMax))
    {
    if (deMin != NULL && limitMin)
        *limitMin = atoi(deMin);
    if (deMax != NULL && limitMax)
        *limitMax = atoi(deMax);
    freeMem(deMin);
    freeMem(deMax);
    }
if ((min || max) && getScoreDefaultsFromTdb(tdb,scoreName,NULL,&deMin,&deMax))
    {
    if (deMin != NULL && min)
        *min = atoi(deMin);
    if (deMax != NULL && max)
        *max =atoi(deMax);
    freeMem(deMin);
    freeMem(deMax);
    }
if (max)
    {
    safef(scoreLimitName, sizeof(scoreLimitName), "%s%s", scoreName, _MAX);
    deMax = cartOptionalStringClosestToHome(cart, tdb,parentLevel,scoreLimitName);
    if (deMax != NULL)
        *max = atoi(deMax);
    }
if (min)
    {                                                           // Warning: name changes if max!
    safef(scoreLimitName, sizeof(scoreLimitName), "%s%s", scoreName, (max && deMax? _MIN:""));
    deMin = cartOptionalStringClosestToHome(cart, tdb,parentLevel,scoreLimitName);
    if (deMin != NULL)
        *min = atoi(deMin);
    }
// Defaulting min and max within limits.  Sorry for the horizontal ifs,
// but stacking the group makes them easier to follow
if (min && limitMin
&& *limitMin != NO_VALUE && (*min == NO_VALUE || *min < *limitMin)) *min = *limitMin;
if (min && limitMax
&& *limitMax != NO_VALUE &&                      *min > *limitMax)  *min = *limitMax;
if (max && limitMax
&& *limitMax != NO_VALUE && (*max == NO_VALUE || *max > *limitMax)) *max = *limitMax;
if (max && limitMin
&& *limitMin != NO_VALUE &&                      *max < *limitMin)  *max = *limitMin;
}

void getScoreFloatRangeFromCart(struct cart *cart, struct trackDb *tdb, boolean parentLevel,
                         char *scoreName, double *limitMin,double *limitMax,double*min,double*max)
// gets an double score range from the cart, but the limits from trackDb
// for any of the pointers provided, will return a value found, if found, else it's contents
// are undisturbed (use NO_VALUE to recognize unavaliable values)
{
char *deMin=NULL,*deMax=NULL;
if ((limitMin || limitMax) && getScoreLimitsFromTdb(tdb,scoreName,NULL,&deMin,&deMax))
    {
    if (deMin != NULL && limitMin)
        *limitMin = strtod(deMin,NULL);
    if (deMax != NULL && limitMax)
        *limitMax =strtod(deMax,NULL);
    freeMem(deMin);
    freeMem(deMax);
    }
if ((min || max) && getScoreDefaultsFromTdb(tdb,scoreName,NULL,&deMin,&deMax))
    {
    if (deMin != NULL && min)
        *min = strtod(deMin,NULL);
    if (deMax != NULL && max)
        *max =strtod(deMax,NULL);
    freeMem(deMin);
    freeMem(deMax);
    }
if (max)
    {
    char *scoreLimitName = getScoreNameAdd(tdb, scoreName, _MAX);
    
    deMax = cartOptionalStringClosestToHome(cart, tdb,parentLevel,scoreLimitName);
    if (deMax != NULL)
        *max = strtod(deMax,NULL);
    }
if (min)
    {                                                // name is always {filterName}Min
    char *scoreLimitName = getScoreNameAdd(tdb, scoreName, _MIN);
    deMin = cartOptionalStringClosestToHome(cart, tdb,parentLevel,scoreLimitName);
    if (deMin == NULL)
        deMin = cartOptionalStringClosestToHome(cart, tdb,parentLevel,scoreName);
    if (deMin != NULL)
        *min = strtod(deMin,NULL);
    }
// Defaulting min and max within limits.  Sorry for the horizontal ifs,
// but stacking the group makes them easier to follow
if (min && limitMin
&& (int)(*limitMin) != NO_VALUE && ((int)(*min) == NO_VALUE || *min < *limitMin)) *min = *limitMin;
if (min && limitMax
&& (int)(*limitMax) != NO_VALUE &&                             *min > *limitMax)  *min = *limitMax;
if (max && limitMax
&& (int)(*limitMax) != NO_VALUE && ((int)(*max) == NO_VALUE || *max > *limitMax)) *max = *limitMax;
if (max && limitMin
&& (int)(*limitMin) != NO_VALUE &&                             *max < *limitMin)  *max = *limitMin;
}

static boolean showScoreFilter(struct cart *cart, struct trackDb *tdb, boolean *opened,
                               boolean boxed, boolean parentLevel,char *name, char *title,
                               char *label, char *scoreName)
// Shows a score filter control with minimum value and optional range
{
char *setting = trackDbSetting(tdb, scoreName);
if (setting)
    {
    if (*opened == FALSE)
        {
        boxed = cfgBeginBoxAndTitle(tdb, boxed, title);
        puts("<TABLE>");
        *opened = TRUE;
        }
    printf("<TR><TD align='right'><B>%s:</B><TD align='left'>",label);
    char varName[256];
    char altLabel[256];
    char *filterName = getScoreNameAdd(tdb, scoreName, _BY_RANGE);
    boolean filterByRange = trackDbSettingClosestToHomeOn(tdb, filterName);
    double minLimit=NO_VALUE,maxLimit=NO_VALUE;
    double minVal=minLimit,maxVal=maxLimit;
    colonPairToDoubles(setting,&minVal,&maxVal);
    getScoreFloatRangeFromCart(cart,tdb,parentLevel,scoreName,&minLimit,&maxLimit,
                                                              &minVal,  &maxVal);
    filterName = getScoreNameAdd(tdb, scoreName, filterByRange ? _MIN:"");
    safef(varName, sizeof(varName), "%s.%s", name, filterName);
    safef(altLabel, sizeof(altLabel), "%s%s", (filterByRange ? "Minimum " : ""),
          htmlEncode(htmlTextStripTags(label)));
    cgiMakeDoubleVarWithLimits(varName,minVal, altLabel, 0,minLimit, maxLimit);
    if (filterByRange)
        {
        printf("<TD align='left'>to<TD align='left'>");
        filterName = getScoreNameAdd(tdb, scoreName, _MAX);
        safef(varName, sizeof(varName), "%s.%s", name, filterName);
        safef(altLabel, sizeof(altLabel), "%s%s", (filterByRange?"Maximum ":""), label);
        cgiMakeDoubleVarWithLimits(varName,maxVal, altLabel, 0,minLimit, maxLimit);
        }
    safef(altLabel, sizeof(altLabel), "%s", (filterByRange?"": "colspan=3"));
    if (minLimit != NO_VALUE && maxLimit != NO_VALUE)
        printf("<TD align='left'%s> (%s to %s)",altLabel,shorterDouble(minLimit), shorterDouble(maxLimit));
    else if (minLimit != NO_VALUE)
        printf("<TD align='left'%s> (minimum %s)",altLabel,shorterDouble(minLimit));
    else if (maxLimit != NO_VALUE)
        printf("<TD align='left'%s> (maximum %s)",altLabel,shorterDouble(maxLimit));
    else
        printf("<TD align='left'%s",altLabel);
    puts("</TR>");
    return TRUE;
    }
return FALSE;
}

struct trackDbFilter *tdbGetTrackFilters( struct trackDb *tdb, char * lowWild, char * lowName, char * capWild, char * capName)
// figure out which of the ways to specify trackDb filter variables we're using
// and return the setting
{
struct trackDbFilter *trackDbFilterList = NULL;
struct slName *filterSettings = trackDbSettingsWildMatch(tdb, lowWild);

if (filterSettings)
    {
    struct trackDbFilter *tdbFilter;
    struct slName *filter = NULL;
    while ((filter = slPopHead(&filterSettings)) != NULL)
        {
        AllocVar(tdbFilter);
        slAddHead(&trackDbFilterList, tdbFilter);
        tdbFilter->name = cloneString(filter->name);
        tdbFilter->setting = trackDbSetting(tdb, filter->name);
        tdbFilter->fieldName = extractFieldNameNew(filter->name, lowName);
        setAsNewFilterType(tdb, tdbFilter->name, tdbFilter->fieldName);
        }
    }
filterSettings = trackDbSettingsWildMatch(tdb, capWild);

if (filterSettings)
    {
    struct trackDbFilter *tdbFilter;
    struct slName *filter = NULL;
    while ((filter = slPopHead(&filterSettings)) != NULL)
        {
        if (differentString(filter->name,NO_SCORE_FILTER))
            {
            AllocVar(tdbFilter);
            slAddHead(&trackDbFilterList, tdbFilter);
            tdbFilter->name = cloneString(filter->name);
            tdbFilter->setting = trackDbSetting(tdb, filter->name);
            tdbFilter->fieldName = extractFieldNameOld(filter->name, capName);
            char *name;
            if ((name = isNewFilterType(tdb, tdbFilter->fieldName) ) != NULL)
                errAbort("error specifying a field's filters in both old (%s) and new format (%s).", tdbFilter->name, name);
            }
        }
    }

return trackDbFilterList;
}

struct trackDbFilter *tdbGetTrackNumFilters( struct trackDb *tdb)
// get the number filters out of trackDb
{
return tdbGetTrackFilters( tdb, FILTER_NUMBER_WILDCARD_LOW, FILTER_NUMBER_NAME_LOW, FILTER_NUMBER_WILDCARD_CAP, FILTER_NUMBER_NAME_CAP);
}

struct trackDbFilter *tdbGetTrackTextFilters( struct trackDb *tdb)
// get the text filters out of trackDb
{
return tdbGetTrackFilters( tdb, FILTER_TEXT_WILDCARD_LOW, FILTER_TEXT_NAME_LOW, FILTER_TEXT_WILDCARD_CAP, FILTER_TEXT_NAME_CAP);
}

struct trackDbFilter *tdbGetTrackFilterByFilters( struct trackDb *tdb)
// get the values filters out of trackDb
{
return tdbGetTrackFilters( tdb, FILTER_VALUES_WILDCARD_LOW, FILTER_VALUES_NAME_LOW, FILTER_VALUES_WILDCARD_CAP, FILTER_VALUES_NAME_CAP);
}

int defaultFieldLocation(char *field)
/* Sometimes we get bigBed filters with field names that are not in the AS file.  
 * Try to guess what the user means. */
{
if (sameString("score", field))
    return 4;
if (sameString("signal", field))
    return 6;
if (sameString("signalValue", field))
    return 6;
if (sameString("pValue", field))
    return 7;
if (sameString("qValue", field))
    return 8;
return -1;
}

static int numericFiltersShowAll(char *db, struct cart *cart, struct trackDb *tdb, boolean *opened,
                                 boolean boxed, boolean parentLevel,char *name, char *title)
// Shows all *Filter style filters.  Note that these are in random order and have no graceful title
{
int count = 0;
struct trackDbFilter *trackDbFilters = tdbGetTrackNumFilters(tdb);
if (trackDbFilters)
    {
    puts("<BR>");
    struct trackDbFilter *filter = NULL;
    struct sqlConnection *conn = NULL;
    if (!isHubTrack(db))
        conn = hAllocConnTrack(db, tdb);
    struct asObject *as = asForTdb(conn, tdb);
    hFreeConn(&conn);

    while ((filter = slPopHead(&trackDbFilters)) != NULL)
        {
        char *field = filter->fieldName;
        char *scoreName = cloneString(filter->name);
        char *trackDbLabel = getLabelSetting(cart, tdb, field);

        if (as != NULL)
            {
            struct asColumn *asCol = asColumnFind(as, field);
            if (asCol != NULL)
                { // Found label so replace field
                field = asCol->comment;
                }
            else if (defaultFieldLocation(field) < 0)
                errAbort("Building filter on field %s which is not in AS file.", field);
            }
        char labelBuf[1024];
        char *label = labelBuf;
        char *filterName = getScoreNameAdd(tdb, scoreName, _BY_RANGE);
        boolean filterByRange = trackDbSettingClosestToHomeOn(tdb, filterName);

        if (trackDbLabel)
            label = trackDbLabel;
        else
            safef(labelBuf, sizeof(labelBuf),"%s%s", filterByRange ? "": "Minimum ", field);

        showScoreFilter(cart,tdb,opened,boxed,parentLevel,name,title,label,scoreName);
        count++;
        }
    if (as != NULL)
        asObjectFree(&as);
    }
if (count > 0)
    puts("</TABLE>");
return count;
}


boolean bedScoreHasCfgUi(struct trackDb *tdb)
// Confirms that this track has a bedScore Cfg UI
{
// Assumes that cfgType == cfgBedScore
if (trackDbSettingClosestToHome(tdb, FILTER_BY))
    return TRUE;
if (trackDbSettingClosestToHome(tdb, GRAY_LEVEL_SCORE_MIN))
    return TRUE;
boolean blocked = FALSE;
struct trackDbFilter *filterSettings = tdbGetTrackNumFilters( tdb);

if (filterSettings != NULL)
    {
    boolean one = FALSE;
    struct trackDbFilter *oneFilter = filterSettings;
    char *noScoreFilter = trackDbSetting(tdb, NO_SCORE_FILTER);
    if (noScoreFilter)
        blocked = TRUE;

    for (;oneFilter != NULL;oneFilter=oneFilter->next)
        {
        if (differentString(oneFilter->fieldName,"score")) // scoreFilter is implicit
            {                                              // but could be blocked
            one = TRUE;
            break;
            }
        }
    if (one)
        return TRUE;
    }
if (!blocked)  // scoreFilter is implicit unless NO_SCORE_FILTER
    return TRUE;

return FALSE;
}


char *getFilterType(struct cart *cart, struct trackDb *tdb, char *field, char *def)
// figure out how the trackDb is specifying the FILTER_TYPE variable and return its setting
{
char settingString[4096];
safef(settingString, sizeof settingString, "%s.%s", FILTER_TYPE_NAME_LOW, field);
char *setting = cartOrTdbString(cart, tdb, settingString, NULL);
if (setting == NULL)
    {
    safef(settingString, sizeof settingString, "%s.%s", field, FILTER_TYPE_NAME_CAP);
    setting = cartOrTdbString(cart, tdb, settingString, NULL);
    }
if (setting == NULL)
    {
    safef(settingString, sizeof settingString, "%s%s", field, FILTER_TYPE_NAME_CAP);
    setting = cartOrTdbString(cart, tdb, settingString, def);
    }
return setting;
}

static int textFiltersShowAll(char *db, struct cart *cart, struct trackDb *tdb)
/* Show all the text filters for this track. */
{
int count = 0;
struct trackDbFilter *trackDbFilters = tdbGetTrackTextFilters(tdb);
if (trackDbFilters)
    {
    puts("<BR>");
    struct trackDbFilter *filter = NULL;
    struct sqlConnection *conn = NULL;
    if (!isHubTrack(db))
        conn = hAllocConnTrack(db, tdb);
    struct asObject *as = asForTdb(conn, tdb);
    hFreeConn(&conn);
    while ((filter = slPopHead(&trackDbFilters)) != NULL)
        {
        char *trackDbLabel = getLabelSetting(cart, tdb, filter->fieldName);
        char *value = cartUsualStringClosestToHome(cart, tdb, FALSE, filter->name, filter->setting);
        if (as != NULL)
            {
            struct asColumn *asCol = asColumnFind(as, filter->fieldName);
            if (asCol != NULL)
                {
                if (trackDbLabel == NULL)
                    trackDbLabel = asCol->comment;
                }
            else if (defaultFieldLocation(filter->fieldName) < 0)
                errAbort("Building filter on field %s which is not in AS file.", filter->fieldName);
            }
        if (trackDbLabel == NULL)
            trackDbLabel = filter->fieldName;

        count++;
        printf("<P><B>Filter items in '%s' field:</B> ", trackDbLabel);

        char cgiVar[128];
        safef(cgiVar,sizeof(cgiVar),"%s.%s",tdb->track,filter->name);
        cgiMakeTextVar(cgiVar, value, 45);

        char *setting = getFilterType(cart, tdb, filter->fieldName, FILTERTEXT_WILDCARD);
        safef(cgiVar,sizeof(cgiVar),"%s.%s.%s",tdb->track,FILTER_TYPE_NAME_LOW, filter->fieldName);
        printf(" using ");
        printf("<SELECT name='%s'> ", cgiVar);
        printf("<OPTION %s>%s</OPTION>", sameString(setting, FILTERTEXT_WILDCARD) ? "SELECTED" : "",  FILTERTEXT_WILDCARD );
        printf("<OPTION %s>%s</OPTION>", sameString(setting, FILTERTEXT_REGEXP) ? "SELECTED" : "",  FILTERTEXT_REGEXP );
        printf("</SELECT>");
        printf("</P>");
        }
    }

return count;
}

void scoreCfgUi(char *db, struct cart *cart, struct trackDb *tdb, char *name, char *title,
                int maxScore, boolean boxed)
// Put up UI for filtering bed track based on a score
{
char option[256];
if (cartOptionalString(cart, "ajax") == NULL)
    {
    webIncludeResourceFile("ui.dropdownchecklist.css");
    jsIncludeFile("ui.dropdownchecklist.js",NULL);
    jsIncludeFile("ddcl.js",NULL);
    }

boolean parentLevel = isNameAtParentLevel(tdb,name);
if (parentLevel)
    if (trackDbSettingOn(tdb->parent, "noParentConfig"))
        return;
boolean skipScoreFilter = FALSE;

// Numeric filters are first
boolean isBoxOpened = FALSE;
if (numericFiltersShowAll(db, cart, tdb, &isBoxOpened, boxed, parentLevel, name, title) > 0)
    skipScoreFilter = TRUE;

if (textFiltersShowAll(db, cart, tdb))
    skipScoreFilter = TRUE;

// Add any multi-selects next
filterBy_t *filterBySet = filterBySetGet(tdb,cart,name);
if (filterBySet != NULL)
    {
    if (!tdbIsComposite(tdb) && cartOptionalString(cart, "ajax") == NULL)
        jsIncludeFile("hui.js",NULL);

    if (!isBoxOpened)   // Note filterBy boxes are not double "boxed",
        printf("<BR>"); // if there are no other filters
    filterBySetCfgUi(cart,tdb,filterBySet,TRUE, name);
    filterBySetFree(&filterBySet);
    skipScoreFilter = TRUE;
    }

boolean scoreFilterOk = (trackDbSettingClosestToHome(tdb, NO_SCORE_FILTER) == NULL) && !skipScoreFilter;
boolean glvlScoreMin = (trackDbSettingClosestToHome(tdb, GRAY_LEVEL_SCORE_MIN) != NULL);
if (! (scoreFilterOk || glvlScoreMin))
    {
    cfgEndBox(boxed);
    return;
    }

boxed = cfgBeginBoxAndTitle(tdb, boxed, title);

if (scoreFilterOk)
    {
    int minLimit=0,maxLimit=maxScore,minVal=0,maxVal=maxScore;
    getScoreIntRangeFromCart(cart,tdb,parentLevel,SCORE_FILTER,&minLimit,&maxLimit,
                                                               &minVal,  &maxVal);

    boolean filterByRange = trackDbSettingClosestToHomeOn(tdb, SCORE_FILTER _BY_RANGE);
    if (filterByRange)
        {
        puts("<B>Filter score range:  min:</B>");
        safef(option, sizeof(option), "%s.%s", name,SCORE_FILTER _MIN);
        cgiMakeIntVarWithLimits(option, minVal, "Minimum score",0, minLimit,maxLimit);
        puts("<B>max:</B>");
        safef(option, sizeof(option), "%s.%s", name,SCORE_FILTER _MAX);
        cgiMakeIntVarWithLimits(option, maxVal, "Maximum score",0,minLimit,maxLimit);
        printf("(%d to %d)\n",minLimit,maxLimit);
        }
    else
        {
        char* scoreLabel = trackDbSettingClosestToHomeOrDefault(tdb, SCORE_LABEL, "score");
        printf("<b>Show only items with %s at or above:</b> ", scoreLabel);
        safef(option, sizeof(option), "%s.%s", name,SCORE_FILTER);
        cgiMakeIntVarWithLimits(option, minVal, "Minimum score",0, minLimit,maxLimit);
        printf("&nbsp;&nbsp;(range: %d to %d)\n", minLimit, maxLimit);
        if (!boxed)
            printf("<BR>\n");
        }
    if (glvlScoreMin)
        printf("<BR>");
    }

if (glvlScoreMin)
    scoreGrayLevelCfgUi(cart, tdb, name, maxScore);

// filter top-scoring N items in track
char *scoreCtString = trackDbSettingClosestToHome(tdb, "filterTopScorers");
if (scoreCtString != NULL)
    {
    // show only top-scoring items. This option only displayed if trackDb
    // setting exists.  Format:  filterTopScorers <on|off> <count> <table>
    char *words[2];
    char *scoreFilterCt = NULL;
    chopLine(cloneString(scoreCtString), words);
    safef(option, sizeof(option), "%s.filterTopScorersOn", name);
    bool doScoreCtFilter =
        cartUsualBooleanClosestToHome(cart, tdb, parentLevel, "filterTopScorersOn",
                                      sameString(words[0], "on"));
    puts("<P>");
    cgiMakeCheckBox(option, doScoreCtFilter);
    safef(option, sizeof(option), "%s.filterTopScorersCt", name);
    scoreFilterCt = cartUsualStringClosestToHome(cart, tdb, parentLevel, "filterTopScorersCt",
                                                 words[1]);

    puts("&nbsp; <B> Show only items in top-scoring </B>");
    cgiMakeIntVarWithLimits(option,atoi(scoreFilterCt),"Top-scoring count",0,1,100000);
    //* Only check size of table if track does not have subtracks */
    if ( !parentLevel && hTableExists(db, tdb->table))
        printf("&nbsp; (range: 1 to 100,000 total items: %d)\n",getTableSize(db, tdb->table));
    else
        printf("&nbsp; (range: 1 to 100,000)\n");
    }
cfgEndBox(boxed);
}

// Moved from hgTrackUi for consistency
static void filterByChromCfgUi(struct cart *cart, struct trackDb *tdb)
{
char filterVar[256];
char *filterVal = "";

printf("<p><b>Filter by chromosome (e.g. chr10):</b> ");
safef(filterVar, sizeof(filterVar), "%s.chromFilter", tdb->track);
(void) cartUsualString(cart, filterVar, filterVal);  // ignore returned setting
cgiMakeTextVar(filterVar, cartUsualString(cart, filterVar, ""), 15);
}

// Moved from hgTrackUi for consistency
void crossSpeciesCfgUi(struct cart *cart, struct trackDb *tdb)
// Put up UI for selecting rainbow chromosome color or intensity score.
{
char colorVar[256];
char *colorSetting;
// initial value of chromosome coloring option is "on", unless
// overridden by the colorChromDefault setting in the track
char *colorDefault = trackDbSettingOrDefault(tdb, "colorChromDefault", "on");

printf("<p><b>Color track based on chromosome:</b> ");
safef(colorVar, sizeof(colorVar), "%s.color", tdb->track);
colorSetting = cartUsualString(cart, colorVar, colorDefault);
cgiMakeRadioButton(colorVar, "on", sameString(colorSetting, "on"));
printf(" on ");
cgiMakeRadioButton(colorVar, "off", sameString(colorSetting, "off"));
printf(" off ");
printf("<br><br>");
filterByChromCfgUi(cart,tdb);
}

struct slPair *buildFieldList(struct trackDb *tdb, char *trackDbVar, struct asObject *as)
/* Build up a hash of a list of fields in an AS file. */
{
char *fields = trackDbSettingClosestToHome(tdb, trackDbVar);

if (fields == NULL)
    return NULL;

if (sameString(fields, "none"))
    return slPairNew("none", NULL);

struct slPair *list = NULL;
struct slName *thisField, *fieldList = slNameListFromComma(fields);
for(thisField = fieldList; thisField; thisField = thisField->next)
    {
    char *trimLabel = trimSpaces(thisField->name);
    unsigned colNum = asColumnFindIx(as->columnList, trimLabel);
    if (colNum == -1)
        errAbort("cannot find field named '%s' in AS file '%s'", 
            trimLabel, as->name);

    slAddHead(&list, slPairNew(trimLabel, NULL + colNum));
    }

slReverse(&list);
return list;
}

void labelCfgUi(char *db, struct cart *cart, struct trackDb *tdb, char *prefix)
/* If there is a labelFields for a bigBed, this routine is called to put up the label options. */
{
if (trackDbSettingClosestToHomeOn(tdb, "linkIdInName"))
    return;

struct asObject *as = asForDb(tdb, db);  
if (as == NULL)
    return;
struct slPair *labelList = buildFieldList(tdb, "labelFields",  as);
struct slPair *defaultLabelList = buildFieldList(tdb, "defaultLabelFields",  as);
char varName[1024];

if ((labelList == NULL) || sameString(labelList->name, "none"))
    return;

printf("<B>Label:</B> ");
struct slPair *thisLabel = labelList;
for(; thisLabel; thisLabel = thisLabel->next)
    {
    safef(varName, sizeof(varName), "%s.label.%s", prefix, thisLabel->name);
    boolean isDefault = FALSE;
    if (defaultLabelList == NULL)
        isDefault = (thisLabel == labelList);
    else if (sameString(defaultLabelList->name, "none"))
        isDefault = FALSE;
    else
        isDefault = (slPairFind(defaultLabelList, thisLabel->name) != NULL);

    boolean option = cartUsualBoolean(cart, varName, isDefault);
    cgiMakeCheckBox(varName, option);

    // find comment for the column listed
    struct asColumn *col = as->columnList;
    unsigned num = ptToInt(thisLabel->val);
    for(; col && num--; col = col->next)
        ;
    assert(col);
    printf(" %s&nbsp;&nbsp;&nbsp;", col->comment);
    }
}

void mergeSpanCfgUi(struct cart *cart, struct trackDb *tdb, char *prefix)
/* If this track offers a merge spanned items option, put up the cfg for it, which
 * is just a checkbox with a small explanation. Comparing tdb->track to prefix
 * ensures we don't offer this control at the composite level, as this is a
 * subtrack only config */
{
if (trackDbSettingOn(tdb, MERGESPAN_TDB_SETTING) && sameString(tdb->track, prefix))
    {
    boolean curOpt = trackDbSettingOn(tdb, "mergeSpannedItems");
    char mergeSetting[256];
    safef(mergeSetting, sizeof(mergeSetting), "%s.%s", tdb->track, MERGESPAN_CART_SETTING);
    if (cartVarExists(cart, mergeSetting))
        curOpt = cartBoolean(cart, mergeSetting);
    printf("<b>Merge items that span the current region</b>:");
    cgiMakeCheckBox(mergeSetting, curOpt);
    }
}

void pslCfgUi(char *db, struct cart *cart, struct trackDb *tdb, char *name, char *title,
              boolean boxed)
/* Put up UI for psl tracks */
{
boxed = cfgBeginBoxAndTitle(tdb, boxed, title);

char *typeLine = cloneString(tdb->type);
char *words[8];
int wordCount = wordCount = chopLine(typeLine, words);
if (sameString(tdb->type, "bigPsl"))
    labelCfgUi(db, cart, tdb, name);
if (wordCount == 3 && sameWord(words[1], "xeno"))
    crossSpeciesCfgUi(cart,tdb);
baseColorDropLists(cart, tdb, name);
indelShowOptionsWithName(cart, tdb, name);
wigOption(cart, name, title, tdb);
snakeOption(cart, name, title, tdb);
cfgEndBox(boxed);
}


void netAlignCfgUi(char *db, struct cart *cart, struct trackDb *tdb, char *prefix, char *title,
                   boolean boxed)
/* Put up UI for net tracks */
{
boxed = cfgBeginBoxAndTitle(tdb, boxed, title);

boolean parentLevel = isNameAtParentLevel(tdb,prefix);

enum netColorEnum netColor = netFetchColorOption(cart, tdb, parentLevel);

char optString[256];    /*      our option strings here */
safef(optString, ArraySize(optString), "%s.%s", prefix, NET_COLOR );
printf("<p><b>Color nets by:&nbsp;</b>");
netColorDropDown(optString, netColorEnumToString(netColor));

#ifdef NOT_YET
enum netLevelEnum netLevel = netFetchLevelOption(cart, tdb, parentLevel);

safef( optString, ArraySize(optString), "%s.%s", prefix, NET_LEVEL );
printf("<p><b>Limit display of nets to:&nbsp;</b>");
netLevelDropDown(optString, netLevelEnumToString(netLevel));
#endif

cfgEndBox(boxed);
}

void chainCfgUi(char *db, struct cart *cart, struct trackDb *tdb, char *prefix, char *title,
                boolean boxed, char *chromosome)
/* Put up UI for chain tracks */
{
boxed = cfgBeginBoxAndTitle(tdb, boxed, title);

boolean parentLevel = isNameAtParentLevel(tdb,prefix);

enum chainColorEnum chainColor = chainFetchColorOption(cart, tdb, parentLevel);

/* check if we have normalized scores available */
boolean normScoreAvailable = chainDbNormScoreAvailable(tdb);

char optString[256];
if (normScoreAvailable)
    {
    safef(optString, ArraySize(optString), "%s.%s", prefix, OPT_CHROM_COLORS );
    printf("<p><b>Color chains by:&nbsp;</b>");
    chainColorDropDown(optString, chainColorEnumToString(chainColor));
    }
else
    {
    printf("<p><b>Color track based on chromosome:</b>&nbsp;");

    char optString[256];
    /* initial value of chromosome coloring option is "on", unless
     * overridden by the colorChromDefault setting in the track */
    char *binaryColorDefault =
	    trackDbSettingClosestToHomeOrDefault(tdb, "colorChromDefault", "on");
    /* allow cart to override trackDb setting */
    safef(optString, sizeof(optString), "%s.color", prefix);
    char * colorSetting = cartUsualStringClosestToHome(cart, tdb,
                                                       parentLevel, "color", binaryColorDefault);
    cgiMakeRadioButton(optString, "on", sameString(colorSetting, "on"));
    printf(" on ");
    cgiMakeRadioButton(optString, "off", sameString(colorSetting, "off"));
    printf(" off ");
    printf("<br>\n");
    }

printf("<p><b>Filter by chromosome (e.g. chr10):</b> ");
safef(optString, ArraySize(optString), "%s.%s", prefix, OPT_CHROM_FILTER);
cgiMakeTextVar(optString, cartUsualStringClosestToHome(cart, tdb, parentLevel,
                                                       OPT_CHROM_FILTER, ""), 15);

if (normScoreAvailable)
    scoreCfgUi(db, cart,tdb,prefix,NULL,CHAIN_SCORE_MAXIMUM,FALSE);


wigOption(cart, prefix, title, tdb);
snakeOption(cart, prefix, title, tdb);
cfgEndBox(boxed);
}

struct dyString *dyAddFilterAsInt(struct cart *cart, struct trackDb *tdb,
                                  struct dyString *extraWhere,char *filter,
                                  char *defaultLimits, char*field, boolean *and)
// creates the where clause condition to support numeric int filter range.
// Filters are expected to follow
//      {fiterName}: trackDb min or min:max - default value(s);
//      {filterName}Min or {filterName}: min (user supplied) cart variable;
//      {filterName}Max: max (user supplied) cart variable;
//      {filterName}Limits: trackDb allowed range "0:1000" Optional
//         uses:{filterName}Min: old trackDb value if {filterName}Limits not found
//              {filterName}Max: old trackDb value if {filterName}Limits not found
//              defaultLimits: function param if no tdb limits settings found)
// The 'and' param and dyString in/out allows stringing multiple where clauses together
{
char filterLimitName[64];
if (sameWord(filter,NO_SCORE_FILTER))
    safef(filterLimitName, sizeof(filterLimitName), "%s", NO_SCORE_FILTER);
else
    safef(filterLimitName, sizeof(filterLimitName), "%s%s", filter,_NO);
if (trackDbSettingClosestToHome(tdb, filterLimitName) != NULL)
    return extraWhere;

char *setting = NULL;
if (differentWord(filter,SCORE_FILTER))
    setting = trackDbSettingClosestToHome(tdb, filter);
else
    setting = trackDbSettingClosestToHomeOrDefault(tdb, filter,"0:1000");
if (setting || sameWord(filter,NO_SCORE_FILTER))
    {
    boolean invalid = FALSE;
    int minValueTdb = 0,maxValueTdb = NO_VALUE;
    colonPairToInts(setting,&minValueTdb,&maxValueTdb);
    int minLimit=NO_VALUE,maxLimit=NO_VALUE,min=minValueTdb,max=maxValueTdb;
    colonPairToInts(defaultLimits,&minLimit,&maxLimit);
    getScoreIntRangeFromCart(cart,tdb,FALSE,filter,&minLimit,&maxLimit,&min,&max);
    if (minLimit != NO_VALUE || maxLimit != NO_VALUE)
        {
        // assume tdb default values within range!
        // (don't give user errors that have no consequence)
        if ((min != minValueTdb && ((minLimit != NO_VALUE && min < minLimit)
                                || (maxLimit != NO_VALUE && min > maxLimit)))
        || (max != maxValueTdb && ((minLimit != NO_VALUE && max < minLimit)
                                || (maxLimit != NO_VALUE && max > maxLimit))))
            {
            invalid = TRUE;
            char value[64];
            if (max == NO_VALUE) // min only is allowed, but max only is not
                safef(value, sizeof(value), "entered minimum (%d)", min);
            else
                safef(value, sizeof(value), "entered range (min:%d and max:%d)", min, max);
            char limits[64];
            if (minLimit != NO_VALUE && maxLimit != NO_VALUE)
                safef(limits, sizeof(limits), "violates limits (%d to %d)", minLimit, maxLimit);
            else if (minLimit != NO_VALUE)
                safef(limits, sizeof(limits), "violates lower limit (%d)", minLimit);
            else //if (maxLimit != NO_VALUE)
                safef(limits, sizeof(limits), "violates uppper limit (%d)", maxLimit);
            warn("invalid filter by %s: %s %s for track %s", field, value, limits, tdb->track);
            }
        }
    // else no default limits!
    if (invalid)
        {
        safef(filterLimitName, sizeof(filterLimitName), "%s%s", filter, (max!=NO_VALUE?_MIN:""));
        cartRemoveVariableClosestToHome(cart,tdb,FALSE,filterLimitName);
        safef(filterLimitName, sizeof(filterLimitName), "%s%s", filter, _MAX);
        cartRemoveVariableClosestToHome(cart,tdb,FALSE,filterLimitName);
        }
    else if ((min != NO_VALUE && (minLimit == NO_VALUE || minLimit != min))
         ||  (max != NO_VALUE && (maxLimit == NO_VALUE || maxLimit != max)))
         // Assumes min==NO_VALUE or min==minLimit is no filter
         // Assumes max==NO_VALUE or max==maxLimit is no filter!
        {
        if (max == NO_VALUE || (maxLimit != NO_VALUE && maxLimit == max))
	    {
	    if (*and) sqlDyStringPrintf(extraWhere, " and ");
            sqlDyStringPrintf(extraWhere, "(%s >= %d)", field, min);  // min only
	    }
        else if (min == NO_VALUE || (minLimit != NO_VALUE && minLimit == min))
	    {
	    if (*and) sqlDyStringPrintf(extraWhere, " and ");
            sqlDyStringPrintf(extraWhere, "(%s <= %d)", field, max);  // max only
	    }
        else
	    {
	    if (*and) sqlDyStringPrintf(extraWhere, " and ");
            sqlDyStringPrintf(extraWhere, "(%s BETWEEN %d and %d)", field, min, max); // both
	    }
        *and=TRUE;
        }
    }
//if (dyStringLen(extraWhere))
//    warn("SELECT FROM %s WHERE %s",tdb->table,dyStringContents(extraWhere));
return extraWhere;
}

struct dyString *dyAddFilterAsDouble(struct cart *cart, struct trackDb *tdb,
                                     struct dyString *extraWhere,char *filter,
                                     char *defaultLimits, char*field, boolean *and)
// creates the where clause condition to support numeric double filters.
// Filters are expected to follow
//      {fiterName}: trackDb min or min:max - default value(s);
//      {filterName}Min or {filterName}: min (user supplied) cart variable;
//      {filterName}Max: max (user supplied) cart variable;
//      {filterName}Limits: trackDb allowed range "0.0:10.0" Optional
//          uses:  defaultLimits: function param if no tdb limits settings found)
// The 'and' param and dyString in/out allows stringing multiple where clauses together
{
char *setting = trackDbSettingClosestToHome(tdb, filter);
if (setting)
    {
    boolean invalid = FALSE;
    double minValueTdb = 0,maxValueTdb = NO_VALUE;
    colonPairToDoubles(setting,&minValueTdb,&maxValueTdb);
    double minLimit=NO_VALUE,maxLimit=NO_VALUE,min=minValueTdb,max=maxValueTdb;
    colonPairToDoubles(defaultLimits,&minLimit,&maxLimit);
    getScoreFloatRangeFromCart(cart,tdb,FALSE,filter,&minLimit,&maxLimit,&min,&max);
    if ((int)minLimit != NO_VALUE || (int)maxLimit != NO_VALUE)
        {
        // assume tdb default values within range!
        // (don't give user errors that have no consequence)
        if ((min != minValueTdb && (((int)minLimit != NO_VALUE && min < minLimit)
                                || ((int)maxLimit != NO_VALUE && min > maxLimit)))
        ||  (max != maxValueTdb && (((int)minLimit != NO_VALUE && max < minLimit)
                                || ((int)maxLimit != NO_VALUE && max > maxLimit))))
            {
            invalid = TRUE;
            char value[64];
            if ((int)max == NO_VALUE) // min only is allowed, but max only is not
                safef(value, sizeof(value), "entered minimum (%g)", min);
            else
                safef(value, sizeof(value), "entered range (min:%g and max:%g)", min, max);
            char limits[64];
            if ((int)minLimit != NO_VALUE && (int)maxLimit != NO_VALUE)
                safef(limits, sizeof(limits), "violates limits (%g to %g)", minLimit, maxLimit);
            else if ((int)minLimit != NO_VALUE)
                safef(limits, sizeof(limits), "violates lower limit (%g)", minLimit);
            else //if ((int)maxLimit != NO_VALUE)
                safef(limits, sizeof(limits), "violates uppper limit (%g)", maxLimit);
            warn("invalid filter by %s: %s %s for track %s", field, value, limits, tdb->track);
            }
        }
    if (invalid)
        {
        char filterLimitName[64];
        safef(filterLimitName, sizeof(filterLimitName), "%s%s", filter, _MIN);
        cartRemoveVariableClosestToHome(cart,tdb,FALSE,filterLimitName);
        safef(filterLimitName, sizeof(filterLimitName), "%s%s", filter, _MAX);
        cartRemoveVariableClosestToHome(cart,tdb,FALSE,filterLimitName);
        }
    else if (((int)min != NO_VALUE && ((int)minLimit == NO_VALUE || minLimit != min))
         ||  ((int)max != NO_VALUE && ((int)maxLimit == NO_VALUE || maxLimit != max)))
         // Assumes min==NO_VALUE or min==minLimit is no filter
         // Assumes max==NO_VALUE or max==maxLimit is no filter!
        {
        if ((int)max == NO_VALUE || ((int)maxLimit != NO_VALUE && maxLimit == max))
	    {
	    if (*and) sqlDyStringPrintf(extraWhere, " and ");
            sqlDyStringPrintf(extraWhere, "(%s >= %g)", field, min);  // min only
	    }
        else if ((int)min == NO_VALUE || ((int)minLimit != NO_VALUE && minLimit == min))
	    {
	    if (*and) sqlDyStringPrintf(extraWhere, " and ");
            sqlDyStringPrintf(extraWhere, "(%s <= %g)", field, max);  // max only
	    }
        else
	    {
	    if (*and) sqlDyStringPrintf(extraWhere, " and ");
            sqlDyStringPrintf(extraWhere, "(%s BETWEEN %g and %g)", field,min,max); // both
	    }
        *and=TRUE;
        }
    }
//if (dyStringLen(extraWhere))
//    warn("SELECT FROM %s WHERE %s",tdb->table,dyStringContents(extraWhere));
return extraWhere;
}

struct dyString *dyAddAllScoreFilters(struct cart *cart, struct trackDb *tdb,
                                      struct dyString *extraWhere,boolean *and)
// creates the where clause condition to gather together all random double filters
// Filters are expected to follow
//      {fiterName}: trackDb min or min:max - default value(s);
//      {filterName}Min or {filterName}: min (user supplied) cart variable;
//      {filterName}Max: max (user supplied) cart variable;
//      {filterName}Limits: trackDb allowed range "0.0:10.0" Optional
//          uses:  defaultLimits: function param if no tdb limits settings found)
// The 'and' param and dyString in/out allows stringing multiple where clauses together
{
struct slName *filterSettings = trackDbSettingsWildMatch(tdb, FILTER_NUMBER_WILDCARD_CAP);
if (filterSettings)
    {
    struct slName *filter = NULL;
    while ((filter = slPopHead(&filterSettings)) != NULL)
        {
        if (differentString(filter->name,"noScoreFilter")
        &&  differentString(filter->name,"scoreFilter")) // TODO: scoreFilter could be included
            {
            char *field = cloneString(filter->name);
            int ix = strlen(field) - strlen("filter");
            assert(ix > 0);
            field[ix] = '\0';
            char *setting = trackDbSetting(tdb, filter->name);
            // How to determine float or int ?
            // If actual tracDb setting has decimal places, then float!
            if (strchr(setting,'.') == NULL)
                extraWhere = dyAddFilterAsInt(cart,tdb,extraWhere,filter->name,"0:1000",field,and);
            else
                extraWhere = dyAddFilterAsDouble(cart,tdb,extraWhere,filter->name,NULL,field,and);
            }
        slNameFree(&filter);
        }
    }
return extraWhere;
}

boolean encodePeakHasCfgUi(struct trackDb *tdb)
// Confirms that this track has encode Peak cfgUI
{
if (sameWord("narrowPeak",tdb->type)
||  sameWord("broadPeak", tdb->type)
||  sameWord("bigNarrowPeak", tdb->type)
||  sameWord("encodePeak",tdb->type)
||  sameWord("gappedPeak",tdb->type))
    {
    return (trackDbSettingClosestToHome(tdb, SCORE_FILTER )
        ||  trackDbSettingClosestToHome(tdb, SIGNAL_FILTER)
        ||  trackDbSettingClosestToHome(tdb, PVALUE_FILTER)
        ||  trackDbSettingClosestToHome(tdb, QVALUE_FILTER)
        ||  trackDbSettingClosestToHome(tdb, SCORE_FILTER ));
    }
return FALSE;
}


void encodePeakCfgUi(struct cart *cart, struct trackDb *tdb, char *name, char *title,
                     boolean boxed)
// Put up UI for filtering wgEnocde peaks based on score, Pval and Qval
{
boolean parentLevel = isNameAtParentLevel(tdb,name);
boolean opened = FALSE;
showScoreFilter(cart,tdb,&opened,boxed,parentLevel,name,title,
                "Minimum Signal value",     SIGNAL_FILTER);
showScoreFilter(cart,tdb,&opened,boxed,parentLevel,name,title,
                "Minimum P-Value (<code>-log<sub>10</sub></code>)",PVALUE_FILTER);
showScoreFilter(cart,tdb,&opened,boxed,parentLevel,name,title,
                "Minimum Q-Value (<code>-log<sub>10</sub></code>)",QVALUE_FILTER);

char *setting = trackDbSettingClosestToHomeOrDefault(tdb, SCORE_FILTER,NULL);//"0:1000");
if (setting)
    {
    if (!opened)
        {
        boxed = cfgBeginBoxAndTitle(tdb, boxed, title);
        puts("<TABLE>");
        opened = TRUE;
        }
    char varName[256];
    int minLimit=0,maxLimit=1000,minVal=0,maxVal=NO_VALUE;
    colonPairToInts(setting,&minVal,&maxVal);
    getScoreIntRangeFromCart(cart,tdb,parentLevel,SCORE_FILTER,&minLimit,&maxLimit,
                                                               &minVal,  &maxVal);
    if (maxVal != NO_VALUE)
        puts("<TR><TD align='right'><B>Score range: min:</B><TD align='left'>");
    else
        puts("<TR><TD align='right'><B>Minimum score:</B><TD align='left'>");
    safef(varName, sizeof(varName), "%s%s", SCORE_FILTER, _BY_RANGE);
    boolean filterByRange = trackDbSettingClosestToHomeOn(tdb, varName);
    safef(varName, sizeof(varName), "%s.%s%s", name, SCORE_FILTER, (filterByRange?_MIN:""));
    cgiMakeIntVarWithLimits(varName, minVal, "Minimum score", 0, minLimit, maxLimit);
    if (filterByRange)
        {
        if (maxVal == NO_VALUE)
            maxVal = maxLimit;
        puts("<TD align='right'>to<TD align='left'>");
        safef(varName, sizeof(varName), "%s.%s%s", name, SCORE_FILTER,_MAX);
        cgiMakeIntVarWithLimits(varName, maxVal, "Maximum score", 0, minLimit, maxLimit);
        }
    printf("<TD align='left'%s> (%d to %d)",(filterByRange?"":" colspan=3"),minLimit, maxLimit);
    if (trackDbSettingClosestToHome(tdb, GRAY_LEVEL_SCORE_MIN) != NULL)
        {
        printf("<TR><TD align='right'colspan=5>");
        scoreGrayLevelCfgUi(cart, tdb, name, 1000);
        puts("</TR>");
        }
    }
if (opened)
    {
    puts("</TABLE>");
    cfgEndBox(boxed);
    }
}

static void gencodeLabelControls(char *db, struct cart *cart, struct trackDb *tdb, char *name, boolean parentLevel)
/* generate label checkboxes for GENCODE. */
{
// See hgTracks/gencodeTracks.c:registerProductionTrackHandlers()
// and hgTracks/gencodeTracks.c:assignConfiguredName()
char *labelsNames[][2] = {
    {"gene name", "geneName"},
    {"gene id", "geneId"},
    {"transcript id", "transcriptId"},
    {NULL, NULL}
};
int i;
for (i = 0; labelsNames[i][0] != NULL; i++)
    {
    char varName[64], varSuffix[64];
    safef(varSuffix, sizeof(varSuffix), "label.%s", labelsNames[i][1]);
    safef(varName, sizeof(varName), "%s.%s", name, varSuffix);
    char *value = cartUsualStringClosestToHome(cart, tdb, parentLevel, varSuffix, NULL);
    boolean checked = (value != NULL) && !sameString(value, "0");
    printf("%s%s: ", (i > 0) ? "&nbsp;&nbsp;" : "", labelsNames[i][0]);
    cgiMakeCheckBoxMore(varName, checked, NULL);
    }
}


static void gencodeMaxTransControl(char *db, struct cart *cart, struct trackDb *tdb, char *name, boolean parentLevel)
{
static char *varSuffix = "maxTrans";
char varName[64];
safef(varName, sizeof(varName), "%s.%s", name, varSuffix);
int maxTrans = cartUsualIntClosestToHome(cart, tdb, parentLevel, varSuffix, 0);
printf("<br>Maximum number of transcripts to display: ");
cgiMakeIntVar(varName, maxTrans, 5);
printf(" (0 to display all)");
}

static void gencodeDisplayControls(char *db, struct cart *cart, struct trackDb *tdb, char *name, boolean parentLevel)
/* generate display controls */
{
if (trackDbSettingClosestToHome(tdb, "maxTransEnabled"))
    gencodeMaxTransControl(db, cart, tdb, name, parentLevel);
}

static void newGencodeShowOptions(struct cart *cart, struct trackDb *tdb)
/* Put up line of controls that describe what parts to show. */
{
char varName[64];

printf("<BR><B>Show:</B> ");

safef(varName, sizeof(varName), "%s.show.noncoding", tdb->track);
boolean option = cartUsualBoolean(cart, varName, TRUE);
cgiMakeCheckBox(varName, option);
printf(" %s&nbsp;&nbsp;&nbsp;", "non-coding genes");

safef(varName, sizeof(varName), "%s.show.spliceVariants", tdb->track);
option = cartUsualBoolean(cart, varName, TRUE);
cgiMakeCheckBox(varName, option);
printf(" %s&nbsp;&nbsp;&nbsp;", "splice variants");

safef(varName, sizeof(varName), "%s.show.pseudo", tdb->track);
option = cartUsualBoolean(cart, varName, FALSE);
cgiMakeCheckBox(varName, option);
printf(" %s&nbsp;&nbsp;&nbsp;", "pseudogenes");

printf("<BR><B>Tagged Sets:</B> ");
safef(varName, sizeof(varName), "%s.show.set", tdb->track);
char *setString = cartUsualString(cart, varName, "basic");
cgiMakeRadioButton(varName, "MANE_Select", sameString(setString, "MANE_Select"));
printf(" %s&nbsp;&nbsp;&nbsp;", "MANE only");
cgiMakeRadioButton(varName, "basic", sameString(setString, "basic"));
printf(" %s&nbsp;&nbsp;&nbsp;", "BASIC only");
cgiMakeRadioButton(varName, "all", sameString(setString, "all"));
printf(" %s&nbsp;&nbsp;&nbsp;", "All");
}

void genePredCfgUi(char *db, struct cart *cart, struct trackDb *tdb, char *name, char *title, boolean boxed)
/* Put up genePred-specific controls */
{
char varName[64];
boolean parentLevel = isNameAtParentLevel(tdb,name);
char *geneLabel = cartUsualStringClosestToHome(cart, tdb,parentLevel, "label", "gene");
boxed = cfgBeginBoxAndTitle(tdb, boxed, title);

labelCfgUi(db, cart, tdb, name);
boolean isGencode3 = trackDbSettingOn(tdb, "isGencode3");

if (sameString(name, "acembly"))
    {
    char *acemblyClass = cartUsualStringClosestToHome(cart,tdb,parentLevel,"type",
                                                      acemblyEnumToString(0));
    printf("<p><b>Gene Class: </b>");
    acemblyDropDown("acembly.type", acemblyClass);
    printf("  ");
    }
else if (isGencode3)
    {
    newGencodeShowOptions(cart, tdb);
    }
else if (startsWith("wgEncodeGencode", name))
    {
    // new GENCODEs
    gencodeLabelControls(db, cart, tdb, name, parentLevel);
    gencodeDisplayControls(db, cart, tdb, name, parentLevel);
    }
else if (sameString("wgEncodeSangerGencode", name)
     ||  (startsWith("encodeGencode", name) && !sameString("encodeGencodeRaceFrags", name)))
    {
    // GENCODE pilot (see hgTracks/gencodeTracks.c:registerPilotTrackHandlers()
    // and hgTracks/simpleTracks.c:genePredAssignConfiguredName()
    printf("<B>Label:</B> ");
    safef(varName, sizeof(varName), "%s.label", name);
    cgiMakeRadioButton(varName, "gene", sameString("gene", geneLabel));
    printf("%s ", "gene");
    cgiMakeRadioButton(varName, "accession", sameString("accession", geneLabel));
    printf("%s ", "accession");
    cgiMakeRadioButton(varName, "both", sameString("both", geneLabel));
    printf("%s ", "both");
    cgiMakeRadioButton(varName, "none", sameString("none", geneLabel));
    printf("%s ", "none");
    }

if (trackDbSettingClosestToHomeOn(tdb, "nmdFilter"))
    {
    boolean nmdDefault = FALSE;
    safef(varName, sizeof(varName), "hgt.%s.nmdFilter", name);
    nmdDefault = cartUsualBoolean(cart,varName, FALSE);
    // TODO: var name (hgt prefix) needs changing before ClosesToHome can be used
    printf("<p><b>Filter out NMD targets.</b>");
    cgiMakeCheckBox(varName, nmdDefault);
    }

if (!sameString(tdb->track, "tigrGeneIndex")
&&  !sameString(tdb->track, "ensGeneNonCoding")
&&  !sameString(tdb->track, "encodeGencodeRaceFrags"))
    baseColorDropLists(cart, tdb, name);

filterBy_t *filterBySet = filterBySetGet(tdb,cart,name);
if (filterBySet != NULL)
    {
    printf("<BR>");
    filterBySetCfgUi(cart,tdb,filterBySet,FALSE, name);
    filterBySetFree(&filterBySet);
    }
filterBy_t *highlightBySet = highlightBySetGet(tdb,cart,name);
if (highlightBySet != NULL)
    {
    printf("<BR>");
    highlightBySetCfgUi(cart,tdb,highlightBySet,FALSE, name);
    filterBySetFree(&highlightBySet);
    }

squishyPackOption(cart, name, title, tdb);
wigOption(cart, name, title, tdb);
cfgEndBox(boxed);
}

static boolean isSpeciesOn(struct cart *cart, struct trackDb *tdb, char *species, char *option, int optionSize, boolean defaultState)
/* check the cart to see if species is turned off or on (default is defaultState) */
{
boolean parentLevel = isNameAtParentLevel(tdb,option);
if (*option == '\0')
    safef(option, optionSize, "%s.%s", tdb->track, species);
else
    {
    char *suffix = option + strlen(option);
    int suffixSize = optionSize - strlen(option);
    safef(suffix,suffixSize,".%s",species);
    }
return cartUsualBooleanClosestToHome(cart,tdb, parentLevel, species,defaultState);
}

char **wigMafGetSpecies(struct cart *cart, struct trackDb *tdb, char *prefix, char *db,
                        struct wigMafSpecies **list, int *groupCt)
{
int speciesCt = 0;
char *speciesGroup   = trackDbSetting(tdb, SPECIES_GROUP_VAR);
char *speciesUseFile = trackDbSetting(tdb, SPECIES_USE_FILE);
char *speciesOrder   = trackDbSetting(tdb, SPECIES_ORDER_VAR);
#define MAX_SP_SIZE 2000
#define MAX_GROUPS 20
char sGroup[MAX_SP_SIZE];
//Ochar *groups[20];
struct wigMafSpecies *wmSpecies, *wmSpeciesList = NULL;
int group;
int i;
char *species[MAX_SP_SIZE];
char option[MAX_SP_SIZE];

*list = NULL;
*groupCt = 0;

/* determine species and groups for pairwise -- create checkboxes */
if (speciesOrder == NULL && speciesGroup == NULL && speciesUseFile == NULL)
    {
    if (isCustomTrack(tdb->track))
	return NULL;
    errAbort("Track %s missing required trackDb setting: speciesOrder, speciesGroups, or speciesUseFile", tdb->track);
    }

char **groups = needMem(MAX_GROUPS * sizeof (char *));
*groupCt = 1;
if (speciesGroup)
    *groupCt = chopByWhite(speciesGroup, groups, MAX_GROUPS);

if (speciesUseFile)
    {
    if ((speciesGroup != NULL) || (speciesOrder != NULL))
	errAbort("Can't specify speciesUseFile and speciesGroup or speciesOrder");
    speciesOrder = cartGetOrderFromFile(db, cart, speciesUseFile);  // Not sure why this is in cart
    }                                                          // not tdb based so no ClosestToHome

for (group = 0; group < *groupCt; group++)
    {
    if (*groupCt != 1 || !speciesOrder)
        {
        safef(sGroup, sizeof sGroup, "%s%s",
                                SPECIES_GROUP_PREFIX, groups[group]);
        speciesOrder = trackDbRequiredSetting(tdb, sGroup);
        }
    speciesCt = chopLine(speciesOrder, species);
    for (i = 0; i < speciesCt; i++)
        {
        AllocVar(wmSpecies);
        wmSpecies->name = cloneString(species[i]);
        safecpy(option,sizeof option,prefix);
	wmSpecies->on = isSpeciesOn(cart, tdb, wmSpecies->name, option, sizeof option, TRUE);
        wmSpecies->group = group;
        slAddHead(&wmSpeciesList, wmSpecies);
        }
    }
slReverse(&wmSpeciesList);
*list = wmSpeciesList;

return groups;
}


struct wigMafSpecies * wigMafSpeciesTable(struct cart *cart,
                                          struct trackDb *tdb, char *name, char *db)
{
int groupCt;
#define MAX_SP_SIZE 2000
char option[MAX_SP_SIZE];
int group, prevGroup;
int i,j;
struct hash *labelHash = mafGetLabelHash(tdb);

bool lowerFirstChar = TRUE;

struct wigMafSpecies *wmSpeciesList;
char **groups = wigMafGetSpecies(cart, tdb, name, db, &wmSpeciesList, &groupCt);
struct wigMafSpecies *wmSpecies = wmSpeciesList;
struct slName *speciesList = NULL;

for(; wmSpecies; wmSpecies = wmSpecies->next)
    {
    struct slName *newName = slNameNew(wmSpecies->name);
    slAddHead(&speciesList, newName);
    }
slReverse(&speciesList);

int numberPerRow;
boolean lineBreakJustPrinted;
char *words[MAX_SP_SIZE];
int defaultOffSpeciesCnt = 0;

if (cartOptionalString(cart, "ajax") == NULL)
    jsIncludeFile("utils.js",NULL);
//jsInit();
puts("\n<P><B>Species selection:</B>&nbsp;");

cgiContinueHiddenVar("g");
char id[256];
PLUS_BUTTON( "id", "plus_pw","cb_maf_","_maf_")
MINUS_BUTTON("id","minus_pw","cb_maf_","_maf_")

char prefix[512];
safef(prefix, sizeof prefix, "%s.", name);
char *defaultOffSpecies = trackDbSetting(tdb, "speciesDefaultOff");
struct hash *offHash = NULL;
if (defaultOffSpecies)
    {
    offHash = newHash(5);
    DEFAULT_BUTTON( "id", "default_pw","cb_maf_","_maf_")
    int wordCt = chopLine(defaultOffSpecies, words);
    defaultOffSpeciesCnt = wordCt;

    /* build hash of species that should be off */
    int ii;
    for(ii=0; ii < wordCt; ii++)
        hashAdd(offHash, words[ii], NULL);
    }

if (groupCt == 1)
    puts("\n<TABLE><TR>");
group = -1;
lineBreakJustPrinted = FALSE;
for (wmSpecies = wmSpeciesList, i = 0, j = 0; wmSpecies != NULL;
		    wmSpecies = wmSpecies->next, i++)
    {
    char *label;
    prevGroup = group;
    group = wmSpecies->group;
    if (groupCt != 1 && group != prevGroup)
	{
	i = 0;
	j = 0;
	if (group != 0)
	    puts("</TR></TABLE>\n");
        /* replace underscores in group names */
        subChar(groups[group], '_', ' ');
        printf("<P>&nbsp;&nbsp;<B><EM>%s</EM></B>", groups[group]);
        printf("&nbsp;&nbsp;");
        safef(option, sizeof(option), "plus_%s", groups[group]);
        PLUS_BUTTON( "id",option,"cb_maf_",groups[group])
        safef(option, sizeof(option),"minus_%s", groups[group]);
        MINUS_BUTTON("id",option,"cb_maf_",groups[group])

        puts("\n<TABLE><TR>");
        }
    numberPerRow = 5;

    /* new logic to decide if line break should be displayed here */
    if ((j != 0 && (j % numberPerRow) == 0) && (lineBreakJustPrinted == FALSE))
        {
        puts("</TR><TR>");
        lineBreakJustPrinted = TRUE;
        }

    char id[MAX_SP_SIZE];
    if (defaultOffSpeciesCnt > 0)
        {
        if (stringArrayIx(wmSpecies->name,words,defaultOffSpeciesCnt) == -1)
            safef(id, sizeof(id), "cb_maf_%s_%s", groups[group], wmSpecies->name);
        else
            {
            safef(id, sizeof(id), "cb_maf_%s_%s_defOff", groups[group], wmSpecies->name);
            }
        }
    else
        safef(id, sizeof(id), "cb_maf_%s_%s", groups[group], wmSpecies->name);

    puts("<TD>");
    boolean defaultState = TRUE;
    if (offHash != NULL)
        defaultState = (hashLookup(offHash, wmSpecies->name) == NULL);
    safecpy(option, sizeof(option), name);
    wmSpecies->on = isSpeciesOn(cart, tdb, wmSpecies->name, option, sizeof option, defaultState );
    cgiMakeCheckBoxWithId(option, wmSpecies->on,id);

    char *remapName = NULL;
    if ((labelHash != NULL) && (remapName = hashFindVal(labelHash,wmSpecies->name)))
        label = remapName;
    else
        {
        label = hOrganism(wmSpecies->name);
        if (label == NULL)
                label = wmSpecies->name;
        if (lowerFirstChar)
            *label = tolower(*label);
        }
    printf("%s<BR>", label);
    puts("</TD>");
    lineBreakJustPrinted = FALSE;
    j++;
    }
puts("</TR></TABLE><BR>\n");
return wmSpeciesList;
}

void wigMafCfgUi(struct cart *cart, struct trackDb *tdb,char *name, char *title, boolean boxed, char *db)
/* UI for maf/wiggle track
 * NOTE: calls wigCfgUi */
{
int i;
char option[MAX_SP_SIZE];
boolean parentLevel = isNameAtParentLevel(tdb,name);

boxed = cfgBeginBoxAndTitle(tdb, boxed, title);

char *defaultCodonSpecies = trackDbSetting(tdb, SPECIES_CODON_DEFAULT);
char *framesTable = trackDbSetting(tdb, "frames");
char *snpTable = trackDbSetting(tdb, "snpTable");
char *treeImage = NULL;
struct consWiggle *consWig, *consWiggles = wigMafWiggles(db, tdb);

boolean isWigMafProt = FALSE;

if (strstr(tdb->type, "wigMafProt")) isWigMafProt = TRUE;

puts("<TABLE><TR><TD VALIGN=\"TOP\">");

if (consWiggles && consWiggles->next)
    {
    /* check for alternate conservation wiggles -- create checkboxes */
    puts("<P STYLE=\"margin-top:10;\"><B>Conservation:</B>" );
    boolean first = TRUE;
    for (consWig = consWiggles; consWig != NULL; consWig = consWig->next)
        {
        char *wigVarSuffix = NULL;
        char *wigVar = wigMafWiggleVar(name, consWig, &wigVarSuffix);
        cgiMakeCheckBox(wigVar,
                        cartUsualBooleanClosestToHome(cart,tdb,parentLevel,wigVarSuffix,first));
        freeMem(wigVar);
        first = FALSE;
        subChar(consWig->uiLabel, '_', ' ');
        printf ("%s&nbsp;", consWig->uiLabel);
        }
    }

struct wigMafSpecies *wmSpeciesList = wigMafSpeciesTable(cart, tdb, name, db);
struct wigMafSpecies *wmSpecies;

if (isWigMafProt)
    puts("<B>Multiple alignment amino acid-level:</B><BR>" );
else
    puts("<B>Multiple alignment base-level:</B><BR>" );

boolean mafDotIsOn = trackDbSettingClosestToHomeOn(tdb, MAF_DOT_VAR);
safef(option, sizeof option, "%s.%s", name, MAF_DOT_VAR);
cgiMakeCheckBox(option, cartUsualBooleanClosestToHome(cart, tdb, parentLevel,MAF_DOT_VAR, mafDotIsOn));

if (isWigMafProt)
    puts("Display amino acids identical to reference as dots<BR>" );
else
    puts("Display bases identical to reference as dots<BR>" );

safef(option, sizeof option, "%s.%s", name, MAF_CHAIN_VAR);
cgiMakeCheckBox(option, cartUsualBooleanClosestToHome(cart,tdb,parentLevel,MAF_CHAIN_VAR,TRUE));

char *irowStr = trackDbSetting(tdb, "irows");
boolean doIrows = (irowStr == NULL) || !sameString(irowStr, "off");
if (isCustomTrack(tdb->track) || doIrows)
    puts("Display chains between alignments<BR>");
else
    {
    if (isWigMafProt)
	puts("Display unaligned amino acids with spanning chain as 'o's<BR>");
    else
        puts("Display unaligned bases with spanning chain as 'o's<BR>");
    }

safef(option, sizeof option, "%s.%s", name, MAF_SHOW_SNP);
if (snpTable)
    {
    printf("<BR><B>Codon Changes:</B><BR>");
    cgiMakeCheckBox(option, cartOrTdbBoolean(cart, tdb, MAF_SHOW_SNP,FALSE));
    puts("Display synonymous and non-synonymous changes in coding exons.<BR>");
    }

safef(option, sizeof option, "%s.%s", name, "codons");
if (framesTable)
    {
    char *nodeNames[512];
    char buffer[128];

    printf("<BR><B>Codon Translation:</B><BR>");
    printf("Default species to establish reading frame: ");
    nodeNames[0] = db;
    for (wmSpecies = wmSpeciesList, i = 1; wmSpecies != NULL;
			wmSpecies = wmSpecies->next, i++)
	{
        nodeNames[i] = wmSpecies->name;
        }
    cgiMakeDropList(SPECIES_CODON_DEFAULT, nodeNames, i,     // tdb independent var
                    cartUsualString(cart, SPECIES_CODON_DEFAULT, defaultCodonSpecies));
    puts("<br>");
    char *cartVal = cartUsualStringClosestToHome(cart, tdb, parentLevel, "codons","codonDefault");
    safef(buffer, sizeof(buffer), "%s.codons",name);
    cgiMakeRadioButton(buffer,"codonNone",     sameWord(cartVal,"codonNone"));
    printf("No codon translation<BR>");
    cgiMakeRadioButton(buffer,"codonDefault",  sameWord(cartVal,"codonDefault"));
    printf("Use default species reading frames for translation<BR>");
    cgiMakeRadioButton(buffer,"codonFrameNone",sameWord(cartVal,"codonFrameNone"));
    printf("Use reading frames for species if available, otherwise no translation<BR>");
    cgiMakeRadioButton(buffer,"codonFrameDef", sameWord(cartVal,"codonFrameDef"));
    printf("Use reading frames for species if available, otherwise use default species<BR>");
    }
else
    {
    /* Codon highlighting does not apply to wigMafProt type */
    if (!strstr(tdb->type, "wigMafProt"))
        {
        puts("<P><B>Codon highlighting:</B><BR>" );

#ifdef GENE_FRAMING

        safef(option, sizeof(option), "%s.%s", name, MAF_FRAME_VAR);
        char *currentCodonMode = cartCgiUsualString(cart, option, MAF_FRAME_GENE);

        /* Disable codon highlighting */
        cgiMakeRadioButton(option, MAF_FRAME_NONE,
                           sameString(MAF_FRAME_NONE, currentCodonMode));
        puts("None &nbsp;");

        /* Use gene pred */
        cgiMakeRadioButton(option, MAF_FRAME_GENE,
                           sameString(MAF_FRAME_GENE, currentCodonMode));
        puts("CDS-annotated frame based on");
        safef(option, sizeof(option), "%s.%s", name, MAF_GENEPRED_VAR);
        genePredDropDown(cart, makeTrackHash(db, chromosome), NULL, option);

#else
        safef(option, sizeof(option), "%s.%s", name, BASE_COLORS_VAR);
        puts("&nbsp; Alternate colors every");
        cgiMakeIntVar(option, cartCgiUsualInt(cart, option, 0), 1);
        puts("bases<BR>");
        safef(option, sizeof(option), "%s.%s", name,
			    BASE_COLORS_OFFSET_VAR);
        puts("&nbsp; Offset alternate colors by");
        cgiMakeIntVar(option, cartCgiUsualInt(cart, option, 0), 1);
        puts("bases<BR>");
#endif
	}
    }

treeImage = trackDbSetting(tdb, "treeImage");
if (treeImage)
    printf("</TD><TD VALIGN=\"TOP\"><IMG SRC=\"../images/%s\"></TD></TR></TABLE>", treeImage);
else
    puts("</TD></TR></TABLE>");

if (trackDbSetting(tdb, CONS_WIGGLE) != NULL)
    {
    wigCfgUi(cart,tdb,name,"Conservation graph:",FALSE);
    }
cfgEndBox(boxed);
}

static char *grayLabels[] =
    { "alignment quality",
      "base qualities",
      "unpaired ends",
    };
static char *grayValues[] =
    { BAM_GRAY_MODE_ALI_QUAL,
      BAM_GRAY_MODE_BASE_QUAL,
      BAM_GRAY_MODE_UNPAIRED,
    };

// When a child input of a radio set is changed, click its radio button:

#define UPDATE_RADIO_FORMAT_JS "\
    var inputs = document.getElementsByName('%s'); \
    if (inputs) { \
      for (var i=0; i < inputs.length; i++) { \
        if (inputs[i].type == 'radio') { \
          inputs[i].checked = (inputs[i].value == '%s'); \
        } \
      } \
    }"

void bamCfgUi(struct cart *cart, struct trackDb *tdb, char *name, char *title, boolean boxed)
/* BAM: short-read-oriented alignment file format. */
{
boxed = cfgBeginBoxAndTitle(tdb, boxed, title);
char cartVarName[1024];

printf("<TABLE%s><TR><TD>",boxed?" width='100%'":"");

bamAddBaseAndIndelSettings(tdb);
// Deal with tdb being from a subtrack when a view is being configured, ugh:
if (differentString(tdb->track, name) && tdb->parent != NULL && sameString(tdb->parent->type, "bam"))
    bamAddBaseAndIndelSettings(tdb->parent);
#ifdef NOTNOW  // temporarily (?) remove this check box because code doesn't allow for setting wiggle options
char *showWig = cartOrTdbString(cart, tdb, BAMWIG_MODE, "0");
safef(cartVarName, sizeof(cartVarName), "%s.%s", name, BAMWIG_MODE);
cgiMakeCheckBox(cartVarName, SETTING_IS_ON(showWig));
printf("</TD><TD>Only show coverage of reads");
#endif
printf("</TD></TR>\n");

printf("<TR><TD>\n");
char *showNames = cartOrTdbString(cart, tdb, BAM_SHOW_NAMES, "0");
safef(cartVarName, sizeof(cartVarName), "%s.%s", name, BAM_SHOW_NAMES);
cgiMakeCheckBox(cartVarName, SETTING_IS_ON(showNames));
printf("</TD><TD>Display read names</TD>");
if (boxed && fileExists(hHelpFile("hgBamTrackHelp")))
    printf("<TD style='text-align:right'><A HREF=\"../goldenPath/help/hgBamTrackHelp.html\" "
           "TARGET=_BLANK>BAM configuration help</A></TD>");
printf("</TR>\n");
boolean canPair = (cartOrTdbString(cart, tdb, BAM_PAIR_ENDS_BY_NAME, NULL) != NULL);
if (canPair)
    {
    char *doPairing = cartOrTdbString(cart, tdb, BAM_PAIR_ENDS_BY_NAME, "0");
    printf("<TR><TD>");
    safef(cartVarName, sizeof(cartVarName), "%s." BAM_PAIR_ENDS_BY_NAME, name);
    cgiMakeCheckBox(cartVarName, SETTING_IS_ON(doPairing));
    printf("</TD><TD>Attempt to join paired end reads by name</TD></TR>\n");
    }
printf("<TR><TD colspan=2>Minimum alignment quality:\n");
safef(cartVarName, sizeof(cartVarName), "%s." BAM_MIN_ALI_QUAL, name);
cgiMakeIntVar(cartVarName,
              atoi(cartOrTdbString(cart, tdb, BAM_MIN_ALI_QUAL, BAM_MIN_ALI_QUAL_DEFAULT)), 4);
printf("</TD></TR></TABLE>");

baseColorDropLists(cart, tdb, name);
puts("<BR>");
indelShowOptionsWithName(cart, tdb, name);
printf("<BR>\n");
printf("<B>Additional coloring modes:</B><BR>\n");
safef(cartVarName, sizeof(cartVarName), "%s." BAM_COLOR_MODE, name);
char *selected = cartOrTdbString(cart, tdb, BAM_COLOR_MODE, BAM_COLOR_MODE_DEFAULT);
cgiMakeRadioButton(cartVarName, BAM_COLOR_MODE_STRAND, sameString(selected, BAM_COLOR_MODE_STRAND));
printf("Color by strand (blue for +, red for -)<BR>\n");
cgiMakeRadioButton(cartVarName, BAM_COLOR_MODE_GRAY, sameString(selected, BAM_COLOR_MODE_GRAY));
printf("Use gray for\n");
char cartVarName2[1024];
safef(cartVarName2, sizeof(cartVarName2), "%s." BAM_GRAY_MODE, name);
int grayMenuSize = canPair ? ArraySize(grayLabels) : ArraySize(grayLabels)-1;
char *sel2 = cartOrTdbString(cart, tdb, BAM_GRAY_MODE, BAM_GRAY_MODE_DEFAULT);
char onChange[2048];
safef(onChange, sizeof(onChange), UPDATE_RADIO_FORMAT_JS,
      cartVarName, BAM_COLOR_MODE_GRAY);
cgiMakeDropListFull(cartVarName2, grayLabels, grayValues, grayMenuSize, sel2, "change", onChange);
printf("<BR>\n");
if (trackDbSettingClosestToHome(tdb, "noColorTag") == NULL)
    {
    cgiMakeRadioButton(cartVarName, BAM_COLOR_MODE_TAG, sameString(selected, BAM_COLOR_MODE_TAG));
    printf("Use R,G,B colors specified in user-defined tag ");
    safef(cartVarName2, sizeof(cartVarName2), "%s." BAM_COLOR_TAG, name);
    sel2 = cartOrTdbString(cart, tdb, BAM_COLOR_TAG, BAM_COLOR_TAG_DEFAULT);
    safef(onChange, sizeof(onChange), UPDATE_RADIO_FORMAT_JS,
	  cartVarName, BAM_COLOR_MODE_TAG);
    cgiMakeTextVarWithJs(cartVarName2, sel2, 30, "keypress", onChange);
    printf("<BR>\n");
    }
cgiMakeRadioButton(cartVarName, BAM_COLOR_MODE_OFF, sameString(selected, BAM_COLOR_MODE_OFF));
printf("No additional coloring");

// let the user choose to see the track in wiggle mode
wigOption(cart, name, title, tdb);

//TODO: include / exclude flags

if (!boxed && fileExists(hHelpFile("hgBamTrackHelp")))
    printf("<P><A HREF=\"../goldenPath/help/hgBamTrackHelp.html\" TARGET=_BLANK>BAM "
           "configuration help</A></P>");

cfgEndBox(boxed);
}

void lrgCfgUi(struct cart *cart, struct trackDb *tdb, char *name, char *title, boolean boxed)
/* LRG: Locus Reference Genomic sequences mapped to assembly. */
{
boxed = cfgBeginBoxAndTitle(tdb, boxed, title);
printf("<TABLE%s><TR><TD>",boxed?" width='100%'":"");
baseColorDrawOptDropDown(cart, tdb);
indelShowOptionsWithNameExt(cart, tdb, name, "LRG sequence", FALSE, FALSE);
cfgEndBox(boxed);
}

void lrgTranscriptAliCfgUi(struct cart *cart, struct trackDb *tdb, char *name, char *title,
			   boolean boxed)
/* LRG Transcripts: Locus Reference Genomic transcript sequences mapped to assembly. */
{
boxed = cfgBeginBoxAndTitle(tdb, boxed, title);
printf("<TABLE%s><TR><TD>",boxed?" width='100%'":"");
baseColorDrawOptDropDown(cart, tdb);
indelShowOptionsWithNameExt(cart, tdb, name, "LRG transcript sequence", FALSE, FALSE);
cfgEndBox(boxed);
}

struct trackDb *rFindView(struct trackDb *forest, char *view)
// Return the trackDb on the list that matches the view tag. Prefers ancestors before decendents
{
struct trackDb *tdb;
for (tdb = forest; tdb != NULL; tdb = tdb->next)
    {
    char *viewSetting = trackDbSetting(tdb, "view");
    if (sameOk(viewSetting, view) || sameOk(tagEncode(viewSetting), view))
        return tdb;
    }
for (tdb = forest; tdb != NULL; tdb = tdb->next)
    {
    struct trackDb *viewTdb = rFindView(tdb->subtracks, view);
    if (viewTdb != NULL)
        return viewTdb;
    }
return NULL;
}

static boolean compositeViewCfgExpandedByDefault(struct trackDb *parentTdb,char *view,
	char **retVisibility)
// returns true if the view cfg is expanded by default.  Optionally allocates string of view
// setting (eg 'dense')
{
boolean expanded = FALSE;
if ( retVisibility != NULL )
    *retVisibility = cloneString(hStringFromTv(parentTdb->visibility));
struct trackDb *viewTdb = rFindView(parentTdb->subtracks, view);
if (viewTdb == NULL)
    return FALSE;
if (retVisibility != NULL)
    *retVisibility = cloneString(hStringFromTv(viewTdb->visibility));
if (trackDbSetting(viewTdb, "viewUi"))
    expanded = TRUE;
return expanded;
}

enum trackVisibility visCompositeViewDefault(struct trackDb *parentTdb,char *view)
// returns the default track visibility of particular view within a composite track
{
char *visibility = NULL;
compositeViewCfgExpandedByDefault(parentTdb,view,&visibility);
enum trackVisibility vis = hTvFromString(visibility);
freeMem(visibility);
return vis;
}

static boolean hCompositeDisplayViewDropDowns(char *db, struct cart *cart, struct trackDb *parentTdb)
// UI for composite view drop down selections.
{
int ix;
char classes[SMALLBUF];
char javascript[JBUFSIZE];
char id[256];
#define CFG_LINK  "<B><A HREF=\"#a_cfg_%s\" id='%s' "\
                  "title=\"%s Configuration\">%s &#9662;</A><INPUT TYPE=HIDDEN " \
                  "NAME='%s.showCfg' value='%s'></B>"
#define CFG_LINK_JS "return (showConfigControls('%s') == false);"
#define MAKE_CFG_LINK(name,title,viewTrack,open) \
		    safef(id, sizeof id, "%s_link", (name)); \
                    printf(CFG_LINK, (name),id,(title),(title),(viewTrack),((open)?"on":"off")); \
		    safef(javascript, sizeof javascript, CFG_LINK_JS, (name)); \
		    jsOnEventById("click", id, javascript);
		    			

// membersForAll is generated once per track, then cached
membersForAll_t *membersForAll = membersForAllSubGroupsGet(parentTdb, cart);
members_t *membersOfView = membersForAll->members[dimV];
if (membersOfView == NULL)
    return FALSE;

char configurable[membersOfView->count];
memset(configurable,cfgNone,sizeof(configurable));
int firstOpened = -1;
boolean makeCfgRows = FALSE;
struct trackDb **matchedViewTracks = needMem(sizeof(struct trackDb *) * membersOfView->count);

for (ix = 0; ix < membersOfView->count; ix++)
    {
    if (membersOfView->subtrackList     != NULL
    &&  membersOfView->subtrackList[ix] != NULL)
        {
        struct trackDb *subtrack = membersOfView->subtrackList[ix]->val;
        matchedViewTracks[ix] = subtrack->parent;
        configurable[ix] = (char)cfgTypeFromTdb(subtrack, TRUE);
        if (configurable[ix] != cfgNone && trackDbSettingBlocksConfiguration(subtrack,FALSE))
            configurable[ix]  = cfgNone;

        if (configurable[ix] != cfgNone)
            {
            if (firstOpened == -1)
                {
                if (cartOrTdbBoolean(cart, matchedViewTracks[ix], "showCfg", FALSE))
                    firstOpened = ix;
                }
            makeCfgRows = TRUE;
            }
        }
    }

toLowerN(membersOfView->groupTitle, 1);
printf("<B>Select %s</B> (<A HREF='../goldenPath/help/multiView.html' title='Help on views' "
       "TARGET=_BLANK>Help</A>):\n", membersOfView->groupTitle);
printf("<TABLE><TR style='text-align:left;'>\n");
// Make row of vis drop downs
for (ix = 0; ix < membersOfView->count; ix++)
    {
    char *viewName = membersOfView->tags[ix];
    if (matchedViewTracks[ix] != NULL)
        {
        printf("<TD>");
        if (configurable[ix] != cfgNone)
            {
            MAKE_CFG_LINK(membersOfView->tags[ix],membersOfView->titles[ix],
                          matchedViewTracks[ix]->track,(firstOpened == ix))
            }
        else
            printf("<B>%s</B>",membersOfView->titles[ix]);
        puts("</TD>");

        char varName[SMALLBUF];
        safef(varName, sizeof(varName), "%s", matchedViewTracks[ix]->track);
        enum trackVisibility tv = hTvFromString(cartUsualString(cart,varName,
                                      hStringFromTv(visCompositeViewDefault(parentTdb,viewName))));

	struct slPair *events = NULL;
        safef(javascript, sizeof(javascript), "matSelectViewForSubTracks(this,'%s');", viewName);
	slPairAdd(&events, "change", cloneString(javascript));
    
        safef(javascript, sizeof(javascript), "this.lastIndex=this.selectedIndex;");
	slPairAdd(&events, "focus", cloneString(javascript));

        printf("<TD>");
        safef(classes, sizeof(classes), "viewDD normalText %s", membersOfView->tags[ix]);
        hTvDropDownClassWithJavascript(varName, NULL, tv, parentTdb->canPack, classes, events);
        puts(" &nbsp; &nbsp; &nbsp;</TD>");
        }
    }
puts("</TR>");

// Make row of cfg boxes if needed
if (makeCfgRows)
    {
    puts("</TABLE><TABLE>");
    for (ix = 0; ix < membersOfView->count; ix++)
        {
        struct trackDb *view = matchedViewTracks[ix];
        if (view != NULL)
            {
            char *viewName = membersOfView->tags[ix];
            printf("<TR id=\"tr_cfg_%s\"", viewName);
            if ((   firstOpened == -1
                 && !compositeViewCfgExpandedByDefault(parentTdb,membersOfView->tags[ix],NULL))
            ||  (firstOpened != -1 && firstOpened != ix))
                printf(" style=\"display:none\"");
            printf("><TD width=10>&nbsp;</TD>");
            int ix2=ix;
            while (0 < ix2--)
                printf("<TD width=100>&nbsp;</TD>");
            printf("<TD colspan=%d>",membersOfView->count+1);
            if (configurable[ix] != cfgNone)
                {                                  // Hint: subtrack is model but named for view
                cfgByCfgType(configurable[ix],db,cart,view->subtracks,view->track,
                             membersOfView->titles[ix],TRUE);
                }
	    printf("</TD></TR>");
            }
        }
    }
puts("</TABLE>");
freeMem(matchedViewTracks);
return TRUE;
}

char *compositeLabelWithVocabLink(char *db,struct trackDb *parentTdb, struct trackDb *childTdb,
	                                char *vocabType, char *label)
// If the parentTdb has a controlledVocabulary setting and the vocabType is found,
// then label will be wrapped with the link to display it.  Return string is cloned.
{
char *vocab = trackDbSetting(parentTdb, "controlledVocabulary");

// WARNING: this is needed to cache metadata in trackDb object (accessed by metadataFindValue)
(void)metadataForTable(db,childTdb,NULL);

if (vocab == NULL)
    return cloneString(label); // No wrapping!

// Currently implemented just for ENCODE style vocab
if (!vocabSettingIsEncode(vocab))
    return cloneString(label);

char *words[SMALLBUF];
int count;
if ((count = chopByWhite(cloneString(vocab), words, SMALLBUF)) <= 1)
    return cloneString(label);


char *suffix = NULL;
char *rootLabel = labelRoot(label, &suffix);

boolean found = FALSE;
int ix;
for (ix=1;ix<count && !found;ix++)
    {
    if (sameString(vocabType,words[ix])) // controlledVocabulary setting matches tag
        {                               // so all labels are linked
        char *link = wgEncodeVocabLink(words[0],"term",words[ix],rootLabel,rootLabel,suffix);
        return link;
        }
    else if (countChars(words[ix],'=') == 1 && childTdb != NULL)
            // The name of a trackDb setting follows and will be the controlled vocab term
        {
        strSwapChar(words[ix],'=',0);
        if (sameString(vocabType,words[ix]))  // tags match, but search for term
            {
            char * cvSetting = words[ix] + strlen(words[ix]) + 1;
            const char * cvTerm = metadataFindValue(childTdb,cvSetting);
            if (cvTerm != NULL)
                {
                char *link = wgEncodeVocabLink(words[0],
                                    (sameWord(cvSetting,"antibody") ?  "target" : "term"),
                                    (char *)cvTerm,(char *)cvTerm,rootLabel,suffix);
                return link;
                }
            }
        }
    }
freeMem(words[0]);
freeMem(rootLabel);
return cloneString(label);
}

#define PM_BUTTON_UC "<IMG height=18 width=18 id='%s' src='../images/%s'>"
#define PM_BUTTON_UC_JS "return (matSetMatrixCheckBoxes(%s%s%s%s%s%s) == false);" 
#define PM_MAKE_BUTTON_UC(s1,s2,s3,s4,s5,s6,name,img) \
    safef(id, sizeof id, "btn_%s", (name)); \
    printf(PM_BUTTON_UC, id, (img)); \
    safef(javascript, sizeof javascript, PM_BUTTON_UC_JS, (s1),(s2),(s3),(s4),(s5),(s6)); \
    jsOnEventById("click", id, javascript);
#define MATRIX_RIGHT_BUTTONS_AFTER 8
#define MATRIX_BOTTOM_BUTTONS_AFTER 20

static void buttonsForAll(boolean left, boolean top)
{
char id[256];
char javascript[1024];
char fullname[256];
safef(fullname, sizeof fullname, "plus_all_%s_%s", left ? "left" : "right", top ? "top" : "bottom");
PM_MAKE_BUTTON_UC("true", "", "", "", "", "",  fullname,    "add_sm.gif")
safef(fullname, sizeof fullname, "minus_all_%s_%s", left ? "left" : "right", top ? "top" : "bottom");
PM_MAKE_BUTTON_UC("false","", "", "", "", "", fullname, "remove_sm.gif")
}

static void buttonsForOne(char *class, boolean vertical, boolean left, boolean top)
{
char id[256];
char javascript[1024];
char fullname[256];
safef(fullname, sizeof fullname, "plus_%s_all_%s_%s" , class, left ? "left" : "right",
                        top ? "top" : "bottom");
PM_MAKE_BUTTON_UC("true",  ",'", class, "'", "", "", fullname,    "add_sm.gif")
if (vertical)
    puts("<BR>");
safef(fullname, sizeof fullname, "minus_%s_all_%s_%s", class, left ? "left" : "right",
                        top ? "top" : "bottom");
PM_MAKE_BUTTON_UC("false", ",'", class, "'", "", "", fullname, "remove_sm.gif")
}

#define MATRIX_SQUEEZE 10
static boolean matrixSqueeze(membersForAll_t* membersForAll)
// Returns non-zero if the matrix will be squeezed.  Non-zero is actually squeezedLabelHeight
{
char *browserVersion;
if (btIE == cgiClientBrowser(&browserVersion, NULL, NULL) && *browserVersion < '9')
    return 0;

members_t *dimensionX = membersForAll->members[dimX];
members_t *dimensionY = membersForAll->members[dimY];
if (dimensionX && dimensionY)
    {
    if (dimensionX->count>MATRIX_SQUEEZE)
        {
        int ixX,cntX=0;
        for (ixX = 0; ixX < dimensionX->count; ixX++)
            {
            if (dimensionX->subtrackList
            &&  dimensionX->subtrackList[ixX]
            &&  dimensionX->subtrackList[ixX]->val)
                cntX++;
            }
        if (cntX>MATRIX_SQUEEZE)
            return TRUE;
        }
    }
return FALSE;
}

static void matrixXheadingsRow1(char *db, struct trackDb *parentTdb, boolean squeeze,
                                membersForAll_t* membersForAll, boolean top)
// prints the top row of a matrix: 'All' buttons; X titles; buttons 'All'
{
members_t *dimensionX = membersForAll->members[dimX];
members_t *dimensionY = membersForAll->members[dimY];

printf("<TR ALIGN=CENTER valign=%s>\n",top?"BOTTOM":"TOP");
if (dimensionX && dimensionY)
    {
    printf("<TH ALIGN=LEFT valign=%s>",top?"TOP":"BOTTOM");
    //printf("<TH ALIGN=LEFT valign=%s>",(top == squeeze)?"BOTTOM":"TOP");//"TOP":"BOTTOM");
    buttonsForAll(TRUE, top);
    puts("&nbsp;All</TH>");
    }

// If there is an X dimension, then titles go across the top
if (dimensionX)
    {
    int ixX,cntX=0;
    if (dimensionY)
        {
        if (squeeze)
            printf("<TH align=RIGHT><div class='%s'><B><EM>%s</EM></B></div></TH>",
                   (top?"up45":"dn45"), dimensionX->groupTitle);
        else
            printf("<TH align=RIGHT><B><EM>%s</EM></B></TH>", dimensionX->groupTitle);
        }
    else
        printf("<TH ALIGN=RIGHT valign=%s>&nbsp;&nbsp;<B><EM>%s</EM></B></TH>",
               (top ? "TOP" : "BOTTOM"), dimensionX->groupTitle);

    for (ixX = 0; ixX < dimensionX->count; ixX++)
        {
        if (dimensionX->subtrackList
        &&  dimensionX->subtrackList[ixX]
        &&  dimensionX->subtrackList[ixX]->val)
            {
            if (dimensionY && squeeze)
                {                                                       // Breaks must be removed!
                strSwapStrs(dimensionX->titles[ixX],strlen(dimensionX->titles[ixX]),"<BR>"," ");
                printf("<TH nowrap='' class='%s'><div class='%s'>%s</div></TH>\n",
                       dimensionX->tags[ixX],(top?"up45":"dn45"),
                       compositeLabelWithVocabLink(db,parentTdb,dimensionX->subtrackList[ixX]->val,
                                                   dimensionX->groupTag,dimensionX->titles[ixX]));
                }
            else
                {
                char *label =replaceChars(dimensionX->titles[ixX]," (","<BR>(");
                printf("<TH WIDTH='60' class='matCell %s all'>&nbsp;%s&nbsp;</TH>",
                       dimensionX->tags[ixX],
                       compositeLabelWithVocabLink(db,parentTdb,dimensionX->subtrackList[ixX]->val,
                                                   dimensionX->groupTag,label));
                freeMem(label);
                }
            cntX++;
            }
        }
    // If dimension is big enough, then add Y buttons to right as well
    if (cntX>MATRIX_RIGHT_BUTTONS_AFTER)
        {
        if (dimensionY)
            {
            if (squeeze)
                printf("<TH align=LEFT><div class='%s'><B><EM>%s</EM></B></div></TH>",
                       (top?"up45":"dn45"), dimensionX->groupTitle);
            else
                printf("<TH align=LEFT><B><EM>%s</EM></B></TH>", dimensionX->groupTitle);
            printf("<TH ALIGN=RIGHT valign=%s>All&nbsp;",top?"TOP":"BOTTOM");
            buttonsForAll(FALSE, top);
            puts("</TH>");
            }
        else
            printf("<TH ALIGN=LEFT valign=%s><B><EM>%s</EM></B>&nbsp;&nbsp;</TH>",
                   top ? "TOP" : "BOTTOM", dimensionX->groupTitle);
        }
    }
else if (dimensionY)
    {
    printf("<TH ALIGN=RIGHT WIDTH=100 nowrap>");
    printf("<B><EM>%s</EM></B>", dimensionY->groupTitle);
    printf("</TH><TH ALIGN=CENTER WIDTH=60>");
    buttonsForAll(FALSE, top);
    puts("</TH>");
    }
puts("</TR>\n");
}

static void matrixXheadingsRow2(struct trackDb *parentTdb, boolean squeeze,
                                membersForAll_t* membersForAll, boolean top)
// prints the 2nd row of a matrix: Y title; X buttons; title Y
{
members_t *dimensionX = membersForAll->members[dimX];
members_t *dimensionY = membersForAll->members[dimY];

// If there are both X and Y dimensions, then there is a row of buttons in X
if (dimensionX && dimensionY)
    {
    int ixX,cntX=0;
    printf("<TR ALIGN=CENTER><TH ALIGN=CENTER colspan=2><B><EM>%s</EM></B></TH>",
           dimensionY->groupTitle);
    for (ixX = 0; ixX < dimensionX->count; ixX++)    // Special row of +- +- +-
        {
        if (dimensionX->subtrackList
        &&  dimensionX->subtrackList[ixX]
        &&  dimensionX->subtrackList[ixX]->val)
            {
            printf("<TD nowrap class='matCell %s all'>\n",dimensionX->tags[ixX]);
            buttonsForOne(dimensionX->tags[ixX], squeeze, TRUE, top);
            puts("</TD>");
            cntX++;
            }
        }
    // If dimension is big enough, then add Y buttons to right as well
    if (cntX>MATRIX_RIGHT_BUTTONS_AFTER)
        printf("<TH ALIGN=CENTER colspan=2><B><EM>%s</EM></B></TH>", dimensionY->groupTitle);
    puts("</TR>\n");
    }
}

static boolean matrixXheadings(char *db,struct trackDb *parentTdb, membersForAll_t* membersForAll,
                               boolean top)
// UI for X headings in matrix
{
boolean squeeze = matrixSqueeze(membersForAll);

if (top)
    matrixXheadingsRow1(db, parentTdb, squeeze, membersForAll, top);

matrixXheadingsRow2(parentTdb, squeeze, membersForAll, top);

if (!top)
    matrixXheadingsRow1(db, parentTdb, squeeze, membersForAll, top);

return squeeze;
}

static void matrixYheadings(char *db,struct trackDb *parentTdb, membersForAll_t* membersForAll,
                            int ixY, boolean left)
// prints the column of Y labels and buttons
{
members_t *dimensionX = membersForAll->members[dimX];
members_t *dimensionY = membersForAll->members[dimY];

struct trackDb *childTdb = NULL;
if (dimensionY
&&  dimensionY->subtrackList
&&  dimensionY->subtrackList[ixY]
&&  dimensionY->subtrackList[ixY]->val)
    childTdb = dimensionY->subtrackList[ixY]->val;

if (dimensionX && dimensionY && childTdb != NULL) // Both X and Y, then column of buttons
    {
    printf("<TH class='matCell all %s' ALIGN=%s nowrap colspan=2>",
           dimensionY->tags[ixY],left?"RIGHT":"LEFT");
    if (left)
        printf("%s&nbsp;",compositeLabelWithVocabLink(db,parentTdb,childTdb,dimensionY->groupTag,
                                                      dimensionY->titles[ixY]));
    buttonsForOne(dimensionY->tags[ixY], FALSE, left, FALSE);
    if (!left)
        printf("&nbsp;%s",compositeLabelWithVocabLink(db,parentTdb,childTdb,dimensionY->groupTag,
                                                      dimensionY->titles[ixY]));
    puts("</TH>");
    }
else if (dimensionX)
    {
    printf("<TH ALIGN=%s>",left?"RIGHT":"LEFT");
    buttonsForAll(TRUE, TRUE);
    puts("</TH>");
    }
else if (left && dimensionY && childTdb != NULL)
    printf("<TH class='matCell all %s' ALIGN=RIGHT nowrap>%s</TH>\n",dimensionY->tags[ixY],
           compositeLabelWithVocabLink(db,parentTdb,childTdb,dimensionY->groupTag,
                                       dimensionY->titles[ixY]));
}

static int displayABCdimensions(char *db,struct cart *cart, struct trackDb *parentTdb,
                                struct slRef *subtrackRefList, membersForAll_t* membersForAll)
// This will walk through all declared nonX&Y dimensions (X and Y is the 2D matrix of CBs.
// NOTE: ABC dims are only supported if there are X & Y both.
//       Also expected number should be passed in
{
int count=0,ix;
for (ix=dimA;ix<membersForAll->dimMax;ix++)
    {
    if (membersForAll->members[ix]==NULL)
        continue;
    if (membersForAll->members[ix]->count<1)
        continue;
    count++;

    if (count==1) // First time set up a table
        puts("<BR><TABLE>");
    printf("<TR><TH valign=top align='right'>&nbsp;&nbsp;<B><EM>%s</EM></B>:</TH>",
           membersForAll->members[ix]->groupTitle);
    int aIx;
    for (aIx=0;aIx<membersForAll->members[ix]->count;aIx++)
        {
        if (membersForAll->members[ix]->tags[aIx] != NULL)
            {
            assert(membersForAll->members[ix]->subtrackList[aIx]->val != NULL);
            printf("<TH align=left nowrap>");
            char objName[SMALLBUF];
            char other[JBUFSIZE];
            boolean alreadySet=FALSE;
            if (membersForAll->members[ix]->selected != NULL)
                alreadySet = membersForAll->members[ix]->selected[aIx];
            safef(objName, sizeof(objName), "%s.mat_%s_dim%c_cb",parentTdb->track,
                  membersForAll->members[ix]->tags[aIx],membersForAll->letters[ix]);
	    safef(other, sizeof other, "class='matCB abc %s'", membersForAll->members[ix]->tags[aIx]);
            cgiMakeCheckBoxIdAndMore(objName,alreadySet,objName,other);
	    jsOnEventById("click", objName, "matCbClick(this);");
            printf("%s",compositeLabelWithVocabLink(db,parentTdb,
                   membersForAll->members[ix]->subtrackList[aIx]->val,
                   membersForAll->members[ix]->groupTag,
                   membersForAll->members[ix]->titles[aIx]));
            puts("</TH>");
            }
        }
    puts("</TR>");
    }
if (count>0)
    puts("</TABLE>");
return count;
}

#ifdef DEBUG
static void dumpDimension(members_t *dimension, char *name, FILE *f)
/* Dump out information on dimension. */
{
int count = dimension->count;
fprintf(f, "%s: count=%d tag=%s title=%s setting=%s<BR>\n", name, count, dimension->tag, dimension->title, dimension->setting);
int i;
for (i=0; i<count; ++i)
    fprintf(f, "%s=%s ", dimension->names[i], dimension->values[i]);
fprintf(f, "<BR>\n");
}
#endif /* DEBUG */

static char *labelWithVocabLinkForMultiples(char *db,struct trackDb *parentTdb, members_t* members)
// If the parentTdb has a controlledVocabulary setting and the vocabType is found,
// then label will be wrapped with the link to all relevent terms.  Return string is cloned.
{
assert(members->subtrackList != NULL);
char *vocab = cloneString(trackDbSetting(parentTdb, "controlledVocabulary"));
if (vocab == NULL)
    return cloneString(members->groupTitle); // No link wrapping!

char *words[15];
int count,ix;
boolean found=FALSE;
if ((count = chopByWhite(vocab, words,15)) <= 1) // vocab now contains just the file name
    return cloneString(members->groupTitle);

char *mdbVar = NULL;

// Find mdb var to look up based upon the groupTag and cv setting
for (ix=1;ix<count && !found;ix++)
    {
    if (sameString(members->groupTag,words[ix])) // controlledVocabulary setting matches tag
        {                                       // so all labels are linked
        mdbVar = members->groupTag;
        break;
        }
    else if (startsWithWordByDelimiter(members->groupTag,'=',words[ix]))
        {
        mdbVar = words[ix] + strlen(members->groupTag) + 1;
        break;
        }
    }
if (mdbVar == NULL)
    {
    freeMem(vocab);
    return cloneString(members->groupTitle);
    }

#define VOCAB_MULTILINK_BEG "<A HREF='hgEncodeVocab?ra=%s&%s=\""
#define VOCAB_MULTILINK_END "\"' title='Click for details of each %s' TARGET=ucscVocab>%s</A>"
struct dyString *dyLink = dyStringCreate(VOCAB_MULTILINK_BEG,vocab,
                                         (sameWord(mdbVar,"antibody")?"target":"term"));

// Now build the comma delimited string of mdb vals (all have same mdb var)
boolean first = TRUE;
for (ix=0;ix<members->count;ix++)
    {
    if (members->subtrackList[ix] != NULL && members->subtrackList[ix]->val != NULL)
        {
        struct trackDb *childTdb = members->subtrackList[ix]->val;
        (void)metadataForTable(db,childTdb,NULL); // Makes sure this has been populated
        const char * mdbVal = metadataFindValue(childTdb,mdbVar); // one for each is enough
        if (mdbVal != NULL)
            {
            if (!first)
                dyStringAppendC(dyLink,',');
            dyStringAppend(dyLink,(char *)mdbVal);
            first = FALSE;
            }
        }
    }
dyStringPrintf(dyLink,VOCAB_MULTILINK_END,members->groupTitle,members->groupTitle);
freeMem(vocab);
return dyStringCannibalize(&dyLink);
}

static boolean compositeUiByFilter(char *db, struct cart *cart, struct trackDb *parentTdb,
                                   char *formName)
// UI for composite tracks: filter subgroups by multiselects to select subtracks.
{
membersForAll_t* membersForAll = membersForAllSubGroupsGet(parentTdb,cart);
if (membersForAll == NULL || membersForAll->filters == FALSE) // Not Matrix or filters
    return FALSE;
if (cartOptionalString(cart, "ajax") == NULL)
    {
    webIncludeResourceFile("ui.dropdownchecklist.css");
    jsIncludeFile("ui.dropdownchecklist.js",NULL);
    jsIncludeFile("ddcl.js",NULL);
    }

cgiDown(0.7);
printf("<B>Select subtracks %sby:</B> (select multiple %sitems - %s)<BR>\n",
       (membersForAll->members[dimX] != NULL || membersForAll->members[dimY] != NULL ? "further ":""),
       (membersForAll->dimMax == dimA?"":"categories and "),FILTERBY_HELP_LINK);
printf("<TABLE><TR valign='top'>\n");

char id[256];
char javascript[1024];

// Do All [+][-] buttons
if (membersForAll->members[dimX] == NULL && membersForAll->members[dimY] == NULL) // No matrix
    {
    printf("<TD align='left' width='50px'><B>All:</B><BR>");
    // TODO: Test when a real world case actually calls this.  Currently no trackDb.ra cases exist
    #define PM_BUTTON_FILTER_COMP "<input type='button' class='inOutButton' id='%s' value='%c'>"
    #define PM_BUTTON_FILTER_COMP_JS "waitOnFunction(filterCompositeSet,this,%s);return false;"
    #define MAKE_PM_BUTTON_FILTER_COMP(tf,fc,plmi) \
    safef(id, sizeof id, "btn_%s", (fc)); \
    printf(PM_BUTTON_FILTER_COMP, id, (plmi)); \
    safef(javascript, sizeof javascript, PM_BUTTON_FILTER_COMP_JS, (tf)); \
    jsOnEventById("click", id, javascript);

    MAKE_PM_BUTTON_FILTER_COMP("true",  "plus_fc",'+')
    MAKE_PM_BUTTON_FILTER_COMP("false","minus_fc",'-')
    printf("</TD>\n");
    }

// Now make a filterComp box for each ABC dimension
int dimIx=dimA;
for (dimIx=dimA;dimIx<membersForAll->dimMax;dimIx++)
    {
    printf("<TD align='left'><B>%s:</B><BR>\n",
           labelWithVocabLinkForMultiples(db,parentTdb,membersForAll->members[dimIx]));

    safef(id, sizeof id, "fc%d",dimIx); 
    printf(
      "<SELECT id='%s' name='%s.filterComp.%s' %s " 
      "style='display: none; font-size:.8em;' " 
      "class='filterComp'><BR>\n" 
	,id,parentTdb->track,membersForAll->members[dimIx]->groupTag,
       "multiple");
    jsOnEventById("change", id, "filterCompositeSelectionChanged(this);");


    // DO we support anything besides multi?
    //  (membersForAll->members[dimIx]->fcType == fctMulti?"multiple ":""));
    if (membersForAll->members[dimIx]->fcType != fctOneOnly)
        printf("<OPTION%s>All</OPTION>\n",
               (sameWord("All",membersForAll->checkedTags[dimIx])?" SELECTED":"") );

    int ix=0;
    for (ix=0;ix<membersForAll->members[dimIx]->count; ix++)
        {
        boolean alreadySet = membersForAll->members[dimIx]->selected[ix];
        printf("<OPTION%s value=%s>%s</OPTION>\n",(alreadySet?" SELECTED":""),
               membersForAll->members[dimIx]->tags[ix],membersForAll->members[dimIx]->titles[ix]);
        }
    printf("</SELECT>");

    if (membersForAll->members[dimIx]->fcType == fctOneOnly)
        printf(" (select only one)");

    printf("</TD><TD width='20'></TD>\n");
    }
printf("</TR></TABLE>\n");

puts("<BR>\n");

return TRUE;
}
void fastMatixToSubtrackMap()
// prints out the "common" globals json hash
// This hash is the one utils.js and therefore all CGIs know about
{
struct dyString *dy = dyStringNew(1024);
dyStringPrintf(dy,
"var mtxSubMap = {};\n"
"$( document ).ready(function()\n"
"{\n"
"matCB = $('input.matCB:first');\n"
"if (!matCB)\n"
"    return;\n"
"var matClassList = $( matCB ).attr('class').split(' ');\n"
"matClassList = aryRemove(matClassList,['matCB','changed','disabled','abc']);\n"
"if (matClassList.length === 0 )\n"
"    return;\n"
"subCBs = $('input.subCB');\n"
"$( subCBs ).each( function (i) { \n"
"  // class='subCB BS-Seq Mantle_Cell_Lymphoma venous_blood A007MCL CNAG CPG_methylation_cov signal' \n"
"  var classList = $( this ).attr('class').split(' ');\n"
"  if (matClassList.length === 1) {\n"
"      var classes = '.' + classList[1]; // dimX or dimY \n"
"  } else {\n"
"      var classes = '.' + classList[1] + '.' + classList[2]; // dimX and dimY \n"
"  }\n"
"  if (mtxSubMap[classes] === undefined) {\n"
"    mtxSubMap[classes] = [this];\n"
"  } else {\n"
"    mtxSubMap[classes].push(this);\n"
"  }\n"
"});\n"
"});\n"
);

jsInline(dy->string);
dyStringFree(&dy);
}


static boolean compositeUiByMatrix(char *db, struct cart *cart, struct trackDb *parentTdb,
                                   char *formName)
// UI for composite tracks: matrix of checkboxes.
{
//int ix;
char objName[SMALLBUF];

membersForAll_t* membersForAll = membersForAllSubGroupsGet(parentTdb,cart);
if (membersForAll == NULL || membersForAll->dimensions == NULL) // Not Matrix!
    return FALSE;

int ixX,ixY;
members_t *dimensionX = membersForAll->members[dimX];
members_t *dimensionY = membersForAll->members[dimY];

// use array of char determine all the cells (in X,Y,Z dimensions) that are actually populated
char *value;
int sizeOfX = dimensionX?dimensionX->count:1;
int sizeOfY = dimensionY?dimensionY->count:1;
int cells[sizeOfX][sizeOfY]; // There needs to be atleast one element in dimension
int chked[sizeOfX][sizeOfY]; // How many subCBs are checked per matCB?
int enabd[sizeOfX][sizeOfY]; // How many subCBs are enabled per matCB?
memset(cells, 0, sizeof(cells));
memset(chked, 0, sizeof(chked));
memset(enabd, 0, sizeof(chked));

struct slRef *subtrackRef, *subtrackRefList =
                                    trackDbListGetRefsToDescendantLeaves(parentTdb->subtracks);
struct trackDb *subtrack;
if (dimensionX || dimensionY) // Must be an X or Y dimension
    {
    // Fill the cells based upon subtrack membership
    for (subtrackRef = subtrackRefList; subtrackRef != NULL; subtrackRef = subtrackRef->next)
        {
        subtrack = subtrackRef->val;
        ixX = (dimensionX ? -1 : 0 );
        ixY = (dimensionY ? -1 : 0 );
        if (dimensionX && subgroupFind(subtrack,dimensionX->groupTag,&value))
            {
            ixX = stringArrayIx(value,dimensionX->tags,dimensionX->count);
            subgroupFree(&value);
            }
        if (dimensionY && subgroupFind(subtrack,dimensionY->groupTag,&value))
            {
            ixY = stringArrayIx(value,dimensionY->tags,dimensionY->count);
            subgroupFree(&value);
            }
        if (ixX > -1 && ixY > -1)
            {
            cells[ixX][ixY]++;
            int fourState = subtrackFourStateChecked(subtrack,cart);
            // hidden views are handled by 4-way CBs: only count enabled
            if (fourStateEnabled(fourState))
                {
                // Only bother if the subtrack is found in all ABC dims checked
                if (subtrackInAllCurrentABCs(subtrack,membersForAll))
                    {
                    enabd[ixX][ixY]++;
                    if (fourStateChecked(fourState) == 1)
                        chked[ixX][ixY]++;
                    }
                }
            }
        }
    }

// If there is no matrix and if there is a filterComposite, then were are done.
if (dimensionX == NULL && dimensionY == NULL)
    {
    if (compositeUiByFilter(db, cart, parentTdb, formName))
        return FALSE;
    }

// Tell the user what to do:
char javascript[JBUFSIZE];
//puts("<B>Select subtracks by characterization:</B><BR>");
printf("<B>Select subtracks by ");
if (dimensionX && !dimensionY)
    safef(javascript, sizeof(javascript), "%s:</B>",dimensionX->groupTitle);
else if (!dimensionX && dimensionY)
    safef(javascript, sizeof(javascript), "%s:</B>",dimensionY->groupTitle);
else if (dimensionX && dimensionY)
    safef(javascript, sizeof(javascript), "%s and %s:</B>",
          dimensionX->groupTitle,dimensionY->groupTitle);
else
    safef(javascript, sizeof(javascript), "multiple variables:</B>");
puts(strLower(javascript));

if (!subgroupingExists(parentTdb,"view"))
    puts("(<A HREF=\"../goldenPath/help/multiView.html\" title='Help on subtrack selection' "
         "TARGET=_BLANK>help</A>)\n");

puts("<BR>\n");

if (membersForAll->abcCount > 0 && membersForAll->filters == FALSE)
    {
    displayABCdimensions(db,cart,parentTdb,subtrackRefList,membersForAll);
    }

// Could have been just filterComposite. Must be an X or Y dimension
if (dimensionX == NULL && dimensionY == NULL)
    return FALSE;

// if there is a treeimage, put it beside the matrix in the green box
char *treeImage =  trackDbSetting(parentTdb, "treeImage");
if (treeImage != NULL)
    {
    printf("<TABLE class='greenBox matrix' ><TD>");
    printf("<TABLE cellspacing=0 style='background-color:%s;'>\n",
       COLOR_BG_ALTDEFAULT);
    }
else
    printf("<TABLE class='greenBox matrix' cellspacing=0 style='background-color:%s;'>\n",
       COLOR_BG_ALTDEFAULT);

(void)matrixXheadings(db,parentTdb,membersForAll,TRUE);

// Now the Y by X matrix
int cntX=0,cntY=0;
for (ixY = 0; ixY < sizeOfY; ixY++)
    {
    if (dimensionY == NULL || (dimensionY->tags[ixY]))
        {
        cntY++;
        assert(!dimensionY || ixY < dimensionY->count);
        printf("<TR ALIGN=CENTER>");

        matrixYheadings(db,parentTdb, membersForAll,ixY,TRUE);

#define MAT_CB_SETUP "<INPUT TYPE=CHECKBOX NAME='%s' ID='%s' VALUE=on %s>"
#define MAT_CB(name,js) printf(MAT_CB_SETUP,(name),(name),(js));
        for (ixX = 0; ixX < sizeOfX; ixX++)
            {
            if (dimensionX == NULL || (dimensionX->tags[ixX]))
                {
                assert(!dimensionX || ixX < dimensionX->count);

                if (cntY==1) // Only do this on the first good Y
                    cntX++;

                if (dimensionX && ixX == dimensionX->count)
                    break;
                char *ttlX = NULL;
                char *ttlY = NULL;
                if (dimensionX)
                    {
                    ttlX = cloneString(dimensionX->titles[ixX]);
                    stripString(ttlX,"<i>");
                    stripString(ttlX,"</i>");
                    }
                if (dimensionY != NULL)
                    {
                    ttlY = cloneString(dimensionY->titles[ixY]);
                    stripString(ttlY,"<i>");
                    stripString(ttlY,"</i>");
                    }
                if (cells[ixX][ixY] > 0)
                    {
                    boolean halfChecked = (  chked[ixX][ixY] > 0
                                          && chked[ixX][ixY] < enabd[ixX][ixY]);

                    struct dyString *dySettings = dyStringNew(256);
                    if (dimensionX && dimensionY)
                        {
                        safef(objName, sizeof(objName), "mat_%s_%s_cb",
                              dimensionX->tags[ixX],dimensionY->tags[ixY]);
                        }
                    else
                        {
                        safef(objName, sizeof(objName), "mat_%s_cb",
                              (dimensionX ? dimensionX->tags[ixX] : dimensionY->tags[ixY]));
                        }
                    if (ttlX && ttlY)
                        printf("<TD class='matCell %s %s'>\n",
                               dimensionX->tags[ixX],dimensionY->tags[ixY]);
                    else
                        printf("<TD class='matCell %s'>\n",
                               (dimensionX ? dimensionX->tags[ixX] : dimensionY->tags[ixY]));
                    dyStringPrintf(dySettings, " class=\"matCB");
                    if (halfChecked)
                        dyStringPrintf(dySettings, " disabled"); // appears disabled but still clickable!
                    if (dimensionX)
                        dyStringPrintf(dySettings, " %s",dimensionX->tags[ixX]);
                    if (dimensionY)
                        dyStringPrintf(dySettings, " %s",dimensionY->tags[ixY]);
                    dyStringAppendC(dySettings,'"');
                    if (chked[ixX][ixY] > 0)
                        dyStringAppend(dySettings," CHECKED");
                    if (halfChecked)
                        dyStringAppend(dySettings," title='Not all associated subtracks have been selected'");

                    MAT_CB(objName,dyStringCannibalize(&dySettings));
                    jsOnEventById("click", objName, "matCbClick(this);"); // X&Y are set by javascript
                    puts("</TD>");
                    }
                else
                    {
                    if (ttlX && ttlY)
                        printf("<TD class='matCell %s %s'></TD>\n",
                               dimensionX->tags[ixX],dimensionY->tags[ixY]);
                    else
                        printf("<TD class='matCell %s'></TD>\n",
                               (dimensionX ? dimensionX->tags[ixX] : dimensionY->tags[ixY]));
                    }
                }
            }
        if (dimensionX && cntX>MATRIX_RIGHT_BUTTONS_AFTER)
            matrixYheadings(db,parentTdb, membersForAll,ixY,FALSE);
        puts("</TR>\n");
        }
    }
if (dimensionY && cntY>MATRIX_BOTTOM_BUTTONS_AFTER)
    matrixXheadings(db,parentTdb,membersForAll,FALSE);

puts("</TABLE>");

// if there is a treeImage, put it beside the matrix
if (treeImage != NULL)
    printf("</TD><TD><IMG SRC=\"%s\"></TD></TABLE>", treeImage);

// If any filter additional filter composites, they can be added at the end.
compositeUiByFilter(db, cart, parentTdb, formName);

fastMatixToSubtrackMap();  

return TRUE;
}

static boolean compositeUiAllButtons(char *db, struct cart *cart, struct trackDb *parentTdb,
                                     char *formName)
// UI for composite tracks: all/none buttons only (as opposed to matrix or lots of buttons
{
if (trackDbCountDescendantLeaves(parentTdb) <= 1)
    return FALSE;

if (dimensionsExist(parentTdb))
    return FALSE;

#define PM_BUTTON_GLOBAL "<IMG height=18 width=18 id='%s' src='../images/%s'>"
#define PM_BUTTON_GLOBAL_JS "matSubCBsCheck(%s);"
char id[256];
safef(id, sizeof id, "btn_plus_all"); 
printf(PM_BUTTON_GLOBAL, id, "add_sm.gif");
jsOnEventByIdF("click", id, PM_BUTTON_GLOBAL_JS, "true");

safef(id, sizeof id, "btn_minus_all"); 
printf(PM_BUTTON_GLOBAL, id, "remove_sm.gif");
jsOnEventByIdF("click", id, PM_BUTTON_GLOBAL_JS, "false");

puts("&nbsp;<B>Select all subtracks</B><BR>");
return TRUE;
}

static boolean compositeUiNoMatrix(char *db, struct cart *cart, struct trackDb *parentTdb,
                                   char *formName)
// UI for composite tracks: subtrack selection.  This is the default UI
// without matrix controls.
{
int i, j, k;
char *words[SMALLBUF];
char option[SMALLBUF];
int wordCnt;
char *name, *value;
char buttonVar[1024];
char setting[] = "subGroupN";
char *button;
struct trackDb *subtrack;
bool hasSubgroups = (trackDbSetting(parentTdb, "subGroup1") != NULL);

if (dimensionsExist(parentTdb))
    return FALSE;

puts("<TABLE>");
if (hasSubgroups)
    {
    puts("<TR><B>Select subtracks:</B></TR>");
    puts("<TR><TD><B><EM>&nbsp; &nbsp; All</EM></B>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;"
         "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </TD><TD>");
    }
else
    {
    puts("<TR><TD><B>All subtracks:</B></TD><TD>");
    }
safef(buttonVar, sizeof buttonVar, "%s", "button_all");
if (formName)
    {
    makeAddClearButtonPair("cpmUiNoMtx_but_all", NULL,"</TD><TD>"); // NULL means all
    }
else
    {
    cgiMakeButton(buttonVar, ADD_BUTTON_LABEL);
    puts("</TD><TD>");
    cgiMakeButton(buttonVar, CLEAR_BUTTON_LABEL);
    }
button = cgiOptionalString(buttonVar);
if (isNotEmpty(button))
    {
    struct slRef *tdbRefList = trackDbListGetRefsToDescendantLeaves(parentTdb->subtracks);
    struct slRef *tdbRef;
    for (tdbRef = tdbRefList; tdbRef != NULL; tdbRef = tdbRef->next)
        {
	subtrack = tdbRef->val;
        boolean newVal = FALSE;
        safef(option, sizeof(option), "%s_sel", subtrack->track);
        newVal = sameString(button, ADD_BUTTON_LABEL);
        cartSetBoolean(cart, option, newVal);
        }
    }
puts("</TD></TR>");
puts("</TABLE>");
// generate set & clear buttons for subgroups
for (i = 0; i < MAX_SUBGROUP; i++)
    {
    char *subGroup;
    safef(setting, sizeof setting, "subGroup%d", i+1);
    if (trackDbSetting(parentTdb, setting) == NULL)
        break;
    wordCnt = chopLine(cloneString(trackDbSetting(parentTdb, setting)), words);
    if (wordCnt < 2)
        continue;
    subGroup = cloneString(words[0]);
    if (sameWord(subGroup,"view"))
        continue;  // Multi-view should have taken care of "view" subgroup already
    puts("<TABLE>");
    printf("<TR><TD><B><EM>&nbsp; &nbsp; %s</EM></B></TD></TR>", words[1]);
    for (j = 2; j < wordCnt; j++)
        {
        if (!parseAssignment(words[j], &name, &value))
            continue;
        printf("<TR><TD>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; %s</TD><TD>",
               value);
        safef(buttonVar, sizeof buttonVar, "%s_%s", subGroup, name);
        if (formName)
            {
	    char id[256];
	    safef(id, sizeof id, "cpmUiNoMtx_but_%d_%d", i, j);
            makeAddClearButtonPair(id, name,"</TD><TD>");
            }
        else
            {
            cgiMakeButton(buttonVar, ADD_BUTTON_LABEL);
            puts("</TD><TD>");
            cgiMakeButton(buttonVar, CLEAR_BUTTON_LABEL);
            }
        puts("</TD></TR>");
        button = cgiOptionalString(buttonVar);
        if (isEmpty(button))
            continue;
	struct slRef *tdbRefList = trackDbListGetRefsToDescendantLeaves(parentTdb->subtracks);
	struct slRef *tdbRef;
	for (tdbRef = tdbRefList; tdbRef != NULL; tdbRef = tdbRef->next)
            {
	    subtrack = tdbRef->val;
            char *p;
            int n;
            if ((p = trackDbSetting(subtrack, "subGroups")) == NULL)
                continue;
            n = chopLine(cloneString(p), words);
            for (k = 0; k < n; k++)
                {
                char *subName, *subValue;
                if (!parseAssignment(words[k], &subName, &subValue))
                    continue;
                if (sameString(subName, subGroup) && sameString(subValue, name))
                    {
                    boolean newVal = FALSE;
                    safef(option, sizeof(option),"%s_sel", subtrack->track);
                    newVal = sameString(button, ADD_BUTTON_LABEL);
                    cartSetBoolean(cart, option, newVal);
                    }
                }
            }
        }
    puts("</TABLE>");
    }
return TRUE;
}

static bool mouseOverJsDone = FALSE;

void printInfoIcon(char *mouseover)
/* Print info icon (i) with explanatory text on mouseover */
{
// see https://www.svgrepo.com/svg/524660/info-circle
printf("<span title=\"%s\">", mouseover);
puts("<svg style='height:1.1em; vertical-align:top' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>");
puts("<circle cx='12' cy='12' r='10' stroke='#1C274C' stroke-width='1.5'/>");
puts("<path d='M12 17V11' stroke='#1C274C' stroke-width='1.5' stroke-linecap='round'/>");
puts("<circle cx='1' cy='1' r='1' transform='matrix(1 0 0 -1 11 9)' fill='#1C274C'/>");
puts("</svg>");
puts("</span>");
if (!mouseOverJsDone)
    {
    jsInline("convertTitleTagsToMouseovers();\n");
    mouseOverJsDone = TRUE;
    }
}

void hCompositeUi(char *db, struct cart *cart, struct trackDb *tdb,
                  char *primarySubtrack, char *fakeSubmit, char *formName)
// UI for composite tracks: subtrack selection.  If primarySubtrack is
// non-NULL, don't allow it to be cleared and only offer subtracks
// that have the same type.  If fakeSubmit is non-NULL, add a hidden
// var with that name so it looks like it was pressed.
{
bool hasSubgroups = (trackDbSetting(tdb, "subGroup1") != NULL);
boolean isMatrix = dimensionsExist(tdb);
boolean viewsOnly = FALSE;

if (primarySubtrack == NULL && !cartVarExists(cart, "ajax"))
    {
    if (trackDbSetting(tdb, "dragAndDrop") != NULL)
        jsIncludeFile("jquery.tablednd.js", NULL);
    jsIncludeFile("ajax.js",NULL);
    jsIncludeFile("hui.js",NULL);
    jsIncludeFile("subCfg.js",NULL);
    }
cgiDown(0.3);

boolean hideSubtracksDefault;
// TODO: Gray out or otherwise suppress when in multi-region mode 
if (compositeHideEmptySubtracksSetting(tdb, &hideSubtracksDefault, NULL, NULL))
    {
    char *hideLabel = "Hide empty subtracks";
    hideLabel = trackDbSettingOrDefault(tdb, SUBTRACK_HIDE_EMPTY_LABEL, hideLabel);
    printf("<p><b>%s:</b> &nbsp;", hideLabel);
    char buf[128];
    safef(buf, sizeof buf, "%s.%s", tdb->track, SUBTRACK_HIDE_EMPTY);
    boolean doHideEmpties = compositeHideEmptySubtracks(cart, tdb, NULL, NULL);
    cgiMakeCheckBox(buf, doHideEmpties);

    // info icon with explanatory text on mouseover
    char *info = 
        "Subtracks with no data in the browser window are hidden. Changing the browser window"
        " by zooming or scrolling may result in display of a different selection of tracks.";
    printInfoIcon(info);
    printf("</p>");
    }

if (trackDbCountDescendantLeaves(tdb) < MANY_SUBTRACKS && !hasSubgroups)
    {
    if (primarySubtrack)
        compositeUiSubtracksMatchingPrimary(db, cart, tdb,primarySubtrack);
    else
        compositeUiSubtracks(db, cart, tdb);
    return;
    }
if (fakeSubmit)
    cgiMakeHiddenVar(fakeSubmit, "submit");

if (primarySubtrack == NULL)
    {
    if (subgroupingExists(tdb,"view"))
        {
        hCompositeDisplayViewDropDowns(db, cart,tdb);
        if (subgroupCount(tdb) <= 1)
            viewsOnly = TRUE;
        }
    if (!viewsOnly)
        {
        cgiDown(0.7);
        if (trackDbSettingOn(tdb, "allButtonPair"))
	    {
            compositeUiAllButtons(db, cart, tdb, formName);
	    }
        else if (!hasSubgroups || !isMatrix)
	    {
            compositeUiNoMatrix(db, cart, tdb, formName);
	    }
        else
	    {
            compositeUiByMatrix(db, cart, tdb, formName);
	    }
        }
    }

cartSaveSession(cart);
cgiContinueHiddenVar("g");

if (primarySubtrack)
    compositeUiSubtracksMatchingPrimary(db, cart, tdb,primarySubtrack);
else
    compositeUiSubtracks(db, cart, tdb);

if (primarySubtrack == NULL)  // primarySubtrack is set for tableBrowser but not hgTrackUi
    {
    if (trackDbCountDescendantLeaves(tdb) > 5)
        {
        cgiDown(0.7);
        cgiMakeButton("Submit", "Submit");
        }
    }
}

boolean superTrackDropDownWithExtra(struct cart *cart, struct trackDb *tdb,
                                    int visibleChild, struct slPair *events)
// Displays hide/show dropdown for supertrack.
// Set visibleChild to indicate whether 'show' should be grayed
// out to indicate that no supertrack members are visible:
//    0 to gray out (no visible children)
//    1 don't gray out (there are visible children)
//   -1 don't know (this function should determine)
// If -1,i the subtracks field must be populated with the child trackDbs.
// Returns false if not a supertrack
{
if (!tdbIsSuperTrack(tdb))
    return FALSE;

// determine if supertrack is show/hide
boolean show = FALSE;
char *setting =
        cartUsualString(cart, tdb->track, tdb->isShow ? "show" : "hide");
if (sameString("show", setting))
    show = TRUE;

// Determine if any tracks in supertrack are visible; if not, the 'show' is grayed out
if (show && (visibleChild == -1))
    {
    visibleChild = 0;
    struct slRef *childRef;
    for ( childRef = tdb->children; childRef != NULL; childRef = childRef->next)
        {
	struct trackDb *cTdb = childRef->val;
        cTdb->visibility =
                hTvFromString(cartUsualString(cart, cTdb->track,
                                      hStringFromTv(cTdb->visibility)));
        if (cTdb->visibility != tvHide)
            visibleChild = 1;
        }
    }
hideShowDropDownWithClassAndExtra(tdb->track, NULL, show, (show && visibleChild) ?
                                  "normalText visDD" : "hiddenText visDD", events);
return TRUE;
}

int tvConvertToNumericOrder(enum trackVisibility v)
{
return ((v) == tvFull   ? 4 : \
        (v) == tvPack   ? 3 : \
        (v) == tvSquish ? 2 : \
        (v) == tvDense  ? 1 : 0);
}

int tvCompare(enum trackVisibility a, enum trackVisibility b)
/* enum trackVis isn't in numeric order by visibility, so compare
 * symbolically: */
{
return (tvConvertToNumericOrder(b) - tvConvertToNumericOrder(a));
}

enum trackVisibility tvMin(enum trackVisibility a, enum trackVisibility b)
/* Return the less visible of a and b. */
{
if (tvCompare(a, b) >= 0)
    return a;
else
    return b;
}

enum trackVisibility tdbLocalVisibility(struct cart *cart, struct trackDb *tdb,
                                        boolean *subtrackOverride)
// returns visibility NOT limited by ancestry.
// Fills optional boolean if subtrack specific vis is found
// If not NULL cart will be examined without ClosestToHome.
// Folders/supertracks resolve to hide/full
{
if (subtrackOverride != NULL)
    *subtrackOverride = FALSE; // default

// tdb->visibility should reflect local trackDb setting
enum trackVisibility vis = tdb->visibility;
if (tdbIsSuperTrack(tdb))
    vis = (tdb->isShow ? tvFull : tvHide);

if (cart != NULL) // cart is optional
    {
    char *cartVis = cartOptionalString(cart, tdb->track);
    boolean cgiVar = FALSE;
    // check hub tracks for visibility settings without the hub prefix
    if (startsWith("hub_", tdb->track) && (cartVis == NULL))
        {
        cartVis = cgiOptionalString( trackHubSkipHubName(tdb->track));
        cgiVar = TRUE;
        }

    if (cartVis != NULL)
        {
        vis = hTvFromString(cartVis);
        if (subtrackOverride != NULL && tdbIsContainerChild(tdb))
            *subtrackOverride = TRUE;
        if (cgiVar)
            {
            cartSetString(cart, tdb->track, cartVis);   // add the decorated visibility to the cart
            cartRemove(cart, trackHubSkipHubName(tdb->track)); // remove the undecorated version
            }
        }
    }
return vis;
}

enum trackVisibility tdbVisLimitedByAncestors(struct cart *cart, struct trackDb *tdb,
                                              boolean checkBoxToo, boolean foldersToo)
// returns visibility limited by ancestry.
// This includes subtrack vis override and parents limit maximum.
// cart may be null, in which case, only trackDb settings (default state) are examined
// checkBoxToo means ensure subtrack checkbox state is visible
// foldersToo means limit by folders (aka superTracks) as well.
{
boolean subtrackOverride = FALSE;
enum trackVisibility vis = tdbLocalVisibility(cart,tdb,&subtrackOverride);

if (tdbIsContainerChild(tdb))
    {
    // subtracks without explicit (cart) vis but are selected, should get inherited vis
    if (!subtrackOverride)
        vis = tvFull;
    // subtracks with checkbox that says no, are stopped cold
    if (checkBoxToo && !fourStateVisible(subtrackFourStateChecked(tdb,cart)))
        vis = tvHide; // Checkbox says no
    }
if (subtrackOverride)
    return vis;
                                                            // aka superTrack
if (vis == tvHide || tdb->parent == NULL || (!foldersToo && tdbIsFolder(tdb->parent)))
    return vis; // end of line

return tvMin(vis,tdbVisLimitedByAncestors(cart,tdb->parent,checkBoxToo,foldersToo));
}

char *compositeViewControlNameFromTdb(struct trackDb *tdb)
// Returns a string with the composite view control name if one exists
{
char *stView   = NULL;
char *name     = NULL;
char *rootName = NULL;
// This routine should give these results: compositeName.viewName or else subtrackName.viewName
// or else compositeName or else subtrackName
if (tdbIsCompositeChild(tdb) == TRUE && trackDbLocalSetting(tdb, "parent") != NULL)
    {
    if (trackDbSettingClosestToHomeOn(tdb, "configurable"))
        rootName = tdb->track;  // subtrackName
    else
        rootName = firstWordInLine(cloneString(trackDbLocalSetting(tdb, "parent")));
    }
if (rootName != NULL)
    {
    if (subgroupFind(tdb,"view",&stView))
        {
        int len = strlen(rootName) + strlen(stView) + 3;
        name = needMem(len);
        safef(name,len,"%s.%s",rootName,stView);
        subgroupFree(&stView);
        }
    else
        name = cloneString(rootName);
    }
else
    name = cloneString(tdb->track);
return name;
}

void compositeViewControlNameFree(char **name)
// frees a string allocated by compositeViewControlNameFromTdb
{
if (name && *name)
    freez(name);
}

boolean isNameAtParentLevel(struct trackDb *tdb,char *name)
// cfgUi controls are passed a prefix name that may be at the composite, view or subtrack level
// returns TRUE if name at view or composite level
{
struct trackDb *parent;
for (parent = tdb->parent; parent != NULL; parent = parent->parent)
    if (startsWithWordByDelimiter(parent->track, '.', name))
        return TRUE;
return FALSE;
}

boolean chainDbNormScoreAvailable(struct trackDb *tdb)
/*      check if normScore column is specified in trackDb as available */
{
boolean normScoreAvailable = FALSE;
char * normScoreTest =
     trackDbSettingClosestToHomeOrDefault(tdb, "chainNormScoreAvailable", "no");
if (differentWord(normScoreTest, "no"))
        normScoreAvailable = TRUE;

return normScoreAvailable;
}

void hPrintAbbreviationTable(struct sqlConnection *conn, char *sourceTable, char *label)
/* Print out table of abbreviations. */
{
char query[256];
sqlSafef(query, sizeof(query), "select name,description from %s order by name", sourceTable);
struct sqlResult *sr = sqlGetResult(conn, query);
webPrintLinkTableStart();
webPrintLabelCell("Symbol");
webPrintLabelCell(label);
char **row;
while ((row = sqlNextRow(sr)) != NULL)
    {
    printf("</TR><TR>\n");
    char *name = row[0];
    char *description = row[1];
    webPrintLinkCell(name);
    webPrintLinkCell(description);
    }
sqlFreeResult(&sr);
webPrintLinkTableEnd();
}

/* Special info (cell type abbreviations) for factorSource tracks */

struct factorSourceInfo 
/* Cell type and description */
    {
    struct factorSourceInfo *next;
    char *name;
    char *description;
    };

static int factorSourceInfoCmp(const void *va, const void *vb)
/* Compare two factorSourceInfo's, sorting on name and then description fields */
{
static char bufA[64], bufB[64];
const struct factorSourceInfo *a = *((struct factorSourceInfo **)va);
const struct factorSourceInfo *b = *((struct factorSourceInfo **)vb);
safef(bufA, 64, "%s+%s", a->name, a->description);
safef(bufB, 64, "%s+%s", b->name, b->description);
return strcmp(bufA, bufB);
}

void hPrintFactorSourceAbbrevTable(struct sqlConnection *conn, struct trackDb *tdb)
/* Print out table of abbreviations. With 'pack' setting, 
 * show cell name only (before '+') and uniqify */
{
char *label = "Cell Type";
char *sourceTable = trackDbRequiredSetting(tdb, SOURCE_TABLE);
char query[256];
sqlSafef(query, sizeof(query), "select name,description from %s order by name", sourceTable);
struct sqlResult *sr = sqlGetResult(conn, query);
webPrintLinkTableStart();
webPrintLabelCell("Symbol");
webPrintLabelCell(label);
char **row;
char *plus;
struct factorSourceInfo *source = NULL, *sources = NULL;
while ((row = sqlNextRow(sr)) != NULL)
    {
    char *name = row[0];
    char *description = row[1];
    // truncate description to just the cell type
    if ((plus = strchr(description, '+')) != NULL)
        *plus = 0;
    AllocVar(source);
    source->name = cloneString(name);
    source->description = cloneString(description);
    slAddHead(&sources, source);
    }
slUniqify(&sources, factorSourceInfoCmp, NULL);
int count = 0;
while ((source = slPopHead(&sources)) != NULL)
    {
    printf("</TR><TR>\n");
    webPrintLinkCell(source->name);
    webPrintLinkCellStart();
    fputs(source->description, stdout);
    count++;
    while (sources && sameString(sources->name, source->name))
        {
        source = slPopHead(&sources);
        fputs(", ", stdout);
        fputs(source->description, stdout);
        count++;
        }
    webPrintLinkCellEnd();
    }
sqlFreeResult(&sr);
webPrintLinkTableEnd();
printf("Total: %d\n", count);
}

static char *makeOnePennantIcon(char *setting, char **hintRet)
// Builds a string with pennantIcon HTML and returns it. Also returns hint. */
{
setting = cloneString(setting);
char *icon = nextWord(&setting);
char buffer[4096];
char *src = NULL;
char *url = NULL, *hint = NULL, *color = NULL;

boolean isTextIcon = FALSE;
if (!(endsWith(icon, ".jpg") || endsWith(icon, ".png")))
    {
    isTextIcon = TRUE;
    color = nextWord(&setting);
    src = strLower(icon);
    }
else if (startsWith("http://", icon) || startsWith("https://", icon) ||
        startsWith("ftp://", icon))
            src = htmlEncode(icon);
else
    {
    safef(buffer, sizeof buffer, "../images/%s", icon);
    src = htmlEncode(buffer);
    }

if (setting)
    {
    url = nextWord(&setting);
    if (setting)
        {
        hint = htmlEncode(stripEnclosingDoubleQuotes(setting));
        }
    }
struct dyString *ds = dyStringNew(0);

// generate markup
if (url)
    dyStringPrintf(ds, "<a class='pennantIconText' href='%s' target='ucscHelp' ", url);
else if (isTextIcon)
    dyStringAppend(ds, "<span class='pennantIconText' ");
if (isTextIcon)
    dyStringPrintf(ds, "style='color: %s;' ", color);
if (hint)
    dyStringPrintf(ds, "title='%s' ", hint);
if (url || isTextIcon)
    dyStringAppend(ds, ">");

// add text or image
if (isTextIcon) 
    dyStringPrintf(ds, "%s", src);
else
    dyStringPrintf(ds, "<img height='16' width='16' src='%s'>", src);

// close tags
if (url)
   dyStringAppend(ds, "</a>");
else if (isTextIcon)
    dyStringAppend(ds, "</span>");
dyStringAppend(ds, "\n");

if (hint && hintRet)
    *hintRet = cloneString(hint);
return dyStringCannibalize(&ds);
}

static struct slPair *makePennantIcons(struct trackDb *tdb)
/* Return a list of pairs of pennantIcon HTML and note strings. */
{
char *setting = trackDbSetting(tdb, "pennantIcon");
if (setting == NULL || sameString(setting, "none"))
    return NULL;
struct slPair *list = NULL;
int maxPennants = 3;
char *pennants[maxPennants];
int numPennants = chopByChar(setting, ';', pennants, ArraySize(pennants));
int i;
for (i = 0;  i < numPennants;  i++)
    {
    char *hint = NULL;
    char *html = makeOnePennantIcon(pennants[i], &hint);
    slPairAdd(&list, html, hint);
    freeMem(html);
    }
slReverse(&list);
return list;
}

void hPrintIcons(struct trackDb *tdb) 
/* prints optional folder and pennants icons and a space, if any icons were printed */
{
bool hasIcon = hPrintPennantIcon(tdb);
if (tdbIsSuper(tdb) || tdbIsComposite(tdb))
    {
    // this is the folder.svg icon from the font-awesome collection.
    // the icon collection also contains a "fa fa-folder-o" icon, which is the outlined version 
    // It was decided to use only the filled out icon for now and use the same icon for super
    // and composite tracks. Adding the SVG removes a dependency and makes the icons show up instantly,
    // instead of the short delay when using fonts. Github uses icons like this.
    hPrintf("<span title='The folder icon indicates a container track. "
            "Click the track name to see all subtracks.'>"
            "<svg class='folderIcon' viewBox='0 0 512 512'><path fill='#00457c' "
            "d='M464 128H272l-64-64H48C21.49 64 0 85.49 0 112v288c0 26.51 21.49 48 48 48h416c26.51 "
            "0 48-21.49 48-48V176c0-26.51-21.49-48-48-48z'/></svg></span>");
    hasIcon = TRUE;
    }
if (hasIcon)
    hPrintf(" ");
}

boolean hPrintPennantIcon(struct trackDb *tdb)
// Returns TRUE and prints out the "pennantIcon" when found.
// Example: ENCODE tracks in hgTracks config list.
{
if (trackDbSetting(tdb, "wgEncode") != NULL)
    {
    hPrintf("<a title='encode project' href='../ENCODE'><img height='16' width='16' "
            "src='../images/encodeThumbnail.jpg'></a>\n");
    }
struct slPair *list = makePennantIcons(tdb), *el;
boolean gotPennant = (list != NULL);
for (el = list;  el != NULL;  el = el->next)
    hPrintf("%s\n", el->name);
slPairFreeValsAndList(&list);

return gotPennant;
}

boolean printPennantIconNote(struct trackDb *tdb)
// Returns TRUE and prints out the "pennantIcon" and note when found.
//This is used by hgTrackUi and hgc before printing out trackDb "html"
{
struct slPair *list = makePennantIcons(tdb), *el;
boolean gotPennant = (list != NULL);
for (el = list;  el != NULL;  el = el->next)
    {
    printf("<br>%s\n", el->name);
    char *hint = el->val;
    if (hint)
        printf("<b>Note:</b> %s\n", hint);
    }
slPairFreeValsAndList(&list);
return gotPennant;
}

void printUpdateTime(char *database, struct trackDb *tdb,
    struct customTrack *ct)
/* display table update time */
{
if (trackHubDatabase(database))
    return;
/* have not decided what to do for a composite container */
if (tdbIsComposite(tdb) || tdbIsSuper(tdb))
    return;
struct sqlConnection *conn = NULL;
char *tableName = NULL;
if (isCustomTrack(tdb->track))
    {
    if (ct)
	{
	conn =  hAllocConn(CUSTOM_TRASH);
	tableName = ct->dbTableName;
	}
    }
else if (startsWith("big", tdb->type))
    {
    char *tableName = hTableForTrack(database, tdb->table);
    struct sqlConnection *conn =  hAllocConnTrack(database, tdb);
    char *bbiFileName = bbiNameFromSettingOrTable(tdb, conn, tableName);
    hFreeConn(&conn);
    struct bbiFile *bbi = NULL;
    if (startsWith("bigWig", tdb->type))
	bbi = bigWigFileOpen(bbiFileName);
    else
	bbi = bigBedFileOpen(bbiFileName);
    time_t timep = 0;
    if (bbi)
	{
	timep = bbiUpdateTime(bbi);
	bbiFileClose(&bbi);
	}
    printBbiUpdateTime(&timep);
    }
else
    {
    tableName = hTableForTrack(database, tdb->table);
    conn = hAllocConnTrack(database, tdb);
    }
if (tableName)
    {
    char *date = firstWordInLine(sqlTableUpdate(conn, tableName));
    if (date != NULL)
	printf("<B>Data last updated at UCSC:&nbsp;</B>%s<BR>\n", date);
    }
hFreeConn(&conn);
}

void printBbiUpdateTime(time_t *timep)
/* for bbi files, print out the timep value */
{
    printf("<B>Data last updated at UCSC:&nbsp;</B>%s<BR>\n", sqlUnixTimeToDate(timep, FALSE));
}

static boolean tableDescriptionsExists(struct sqlConnection *conn)
/* Cache flag for whether tableDescriptions exists in conn, in case we will need to
 * fetch a lot of descriptions from tableDescriptions. */
{
static struct hash *hash = NULL;
if (hash == NULL)
    hash = hashNew(0);
char *db = sqlGetDatabase(conn);
int exists =  hashIntValDefault(hash, db, -1);
if (exists < 0)
    {
    exists = sqlTableExists(conn, "tableDescriptions");
    hashAddInt(hash, db, exists);
    }
return (boolean)exists;
}

struct asObject *asFromTableDescriptions(struct sqlConnection *conn, char *table)
// If there is a tableDescriptions table and it has an entry for table, return
// a parsed autoSql object; otherwise return NULL.
{
struct asObject *asObj = NULL;
if (tableDescriptionsExists(conn))
    {
    char query[PATH_LEN*2];
    // Try unsplit table first.
    sqlSafef(query, sizeof(query),
             "select autoSqlDef from tableDescriptions where tableName='%s'", table);
    char *asText = sqlQuickString(conn, query);
    // If no result try split table.
    if (asText == NULL)
        {
        sqlSafef(query, sizeof(query),
                 "select autoSqlDef from tableDescriptions where tableName='chrN_%s'", table);
        asText = sqlQuickString(conn, query);
        }
    if (isNotEmpty(asText))
        asObj = asParseText(asText);
    freez(&asText);
    }
return asObj;
}

static struct asObject *asForTdbOrDie(struct sqlConnection *conn, struct trackDb *tdb)
// Get autoSQL description if any associated with tdb.
// Abort if there's a problem
{
struct asObject *asObj = NULL;
if (tdbIsBigBed(tdb))
    {
    char *fileName = hReplaceGbdb(tdbBigFileName(conn, tdb));
    if (fileName == NULL)
        return NULL;
    asObj = bigBedFileAsObjOrDefault(fileName);
    freeMem(fileName);
    }
// TODO: standardize to a wig as
//else if (tdbIsBigWig(tdb))
//    asObj = asObjFrombigBed(conn,tdb);
else if (tdbIsLongTabix(tdb))
    asObj = longTabixAsObj();
else if (tdbIsBam(tdb))
    asObj = bamAsObj();
else if (tdbIsVcf(tdb))
    asObj = vcfAsObj();
else if (startsWithWord("makeItems", tdb->type))
    asObj = makeItemsItemAsObj();
else if (sameWord("bedDetail", tdb->type))
    asObj = bedDetailAsObj();
else if (sameWord("pgSnp", tdb->type))
    asObj = pgSnpAsObj();
else if (sameWord("barChart", tdb->type))
    asObj = asParseText(barChartAutoSqlString);
else if (sameWord("interact", tdb->type))
    asObj = interactAsObj();
else if (sameWord("hic", tdb->type))
    // HI-C data are stored in .hic files, but parsed into interact objects
    asObj = interactAsObj();
else
    asObj = asFromTableDescriptions(conn, tdb->table);
return asObj;
}

struct asObject *asForTdb(struct sqlConnection *conn, struct trackDb *tdb)
// Get autoSQL description if any associated with table, ignoring errAborts if any.
{
struct errCatch *errCatch = errCatchNew();
struct asObject *asObj = NULL;
// Wrap some error catching around asForTdbOrDie.
if (errCatchStart(errCatch))
    {
    asObj = asForTdbOrDie(conn, tdb);
    }
errCatchEnd(errCatch);
errCatchFree(&errCatch);
return asObj;
}

struct asObject *asForDb(struct trackDb *tdb, char* database)
/* return asObject given the database. NULL if not found */
{
struct sqlConnection *conn = NULL ;
if (!trackHubDatabase(database))
    conn = hAllocConnTrack(database, tdb);
struct asObject *as = asForTdb(conn, tdb);
hFreeConn(&conn);
return as;
}

#ifdef OLD /* This got moved to main library . */
struct asColumn *asColumnFind(struct asObject *asObj, char *name)
// Return named column.
{
struct asColumn *asCol = NULL;
if (asObj!= NULL)
    {
    for (asCol = asObj->columnList; asCol != NULL; asCol = asCol->next)
        if (sameString(asCol->name, name))
            break;
    }
return asCol;
}
#endif /* OLD */

struct slName *asColNames(struct asObject *as)
// Get list of column names.
{
struct slName *list = NULL, *el;
struct asColumn *col;
for (col = as->columnList; col != NULL; col = col->next)
    {
    el = slNameNew(col->name);
    slAddHead(&list, el);
    }
slReverse(&list);
return list;
}

static struct dyString *subMultiField(char *pattern, int fieldCount,
                                 char *in[], char *out[])
/* Substitute $in with out values in pattern */
{
int i;
struct dyString *s = dyStringNew(256), *d = NULL;
dyStringAppend(s, pattern);
for (i=0; i<fieldCount; ++i)
    {
    if (out[i]==NULL)
        continue;

    // If a field is a prefix or suffix to another field, for example 'chrom' and 'chromStart'
    // we don't want to erroneously sub out the 'chrom' in 'chromStart'. Allow the wrapping
    // protected fields in ${} to prevent the substitution:
    char *field = in[i];
    int fieldLen = strlen(field);
    char *spec = needMem(fieldLen + 2);
    char *strictSpec = needMem(fieldLen + 4);
    *spec = '$';
    *strictSpec = '$';
    strictSpec[1] = '{';
    strcpy(spec + 1, field);
    strcpy(strictSpec + 2, field);
    strictSpec[fieldLen + 2] = '}';
    strictSpec[fieldLen + 3] = '\0';

    if (stringIn(strictSpec, s->string))
        {
        d = dyStringSub(s->string, strictSpec, out[i]);
        s = d;
        }
    // the user may have both a ${} enclosed instance and a non-enclosed one!
    d = dyStringSub(s->string, spec, out[i]);

    dyStringFree(&s);
    freeMem(spec);
    freeMem(strictSpec);
    s = d;
    d = NULL;
    }
return s;
}

char *replaceFieldInPattern(char *pattern, int fieldCount, char **fieldNames, char **fieldVals)
/* Replace $fieldName in pattern with value.  Used in trackDb mouseOver setting */
{
struct dyString *ds = subMultiField(pattern, fieldCount, fieldNames, fieldVals);
return dyStringCannibalize(&ds);
}

static struct dyString *subMulti(char *orig, int subCount,
                                 char *in[], char *out[])
/* Perform multiple substitions on orig. */
{
int i;
struct dyString *s = dyStringNew(256), *d = NULL;

dyStringAppend(s, orig);
for (i=0; i<subCount; ++i)
    {
    fflush(stdout);
    if (out[i]==NULL)
        continue;
    d = dyStringSub(s->string, in[i], out[i]);
    dyStringFree(&s);
    s = d;
    d = NULL;
    }
return s;
}

char *replaceInUrl(char *url, char *idInUrl, struct cart *cart, char *db, char *seqName, 
                        int winStart, int winEnd, char *track, boolean encode, struct slPair *fields) 
/* replace $$ in url with idInUrl. Supports many other wildchards, and custom fields $<field>
 * XX Do we have readable docs for these parameters somewhere?
 * Look at http://genome.ucsc.edu/goldenpath/help/trackDb/trackDbHub.html */
{
struct dyString *uUrl = NULL;
struct dyString *eUrl = NULL;
char startString[64], endString[64],oneBasedStart[64];
char *ins[14], *outs[14];
char *eItem = (encode ? cgiEncode(idInUrl) : cloneString(idInUrl));

char *scName = NULL;
// try to avoid the mysql query it not necessary
if (stringIn("$n", url))
    {
    char *tmp = hScientificName(db);
    scName = replaceChars(tmp, " ", "_");
    freeMem(tmp);
    }

char *taxId = NULL;
// try to avoid the mysql query it not necessary
if (stringIn("$taxId", url))
    {
    char query[256];
    struct sqlConnection *centralConn = hConnectCentral();
    sqlSafef(query, sizeof(query),
        "select taxId from %s "
	"where name='%s'", dbDbTable(), db);
    taxId = sqlQuickString(centralConn, query);
    hDisconnectCentral(&centralConn);
    }

safef(startString, sizeof startString, "%d", winStart);
safef(endString, sizeof endString, "%d", winEnd);
ins[0] = "$$";
outs[0] = idInUrl;
ins[1] = "$T";
outs[1] = track;
ins[2] = "$S";
outs[2] = seqName;
ins[3] = "$[";
outs[3] = startString;
ins[4] = "$]";
outs[4] = endString;
ins[5] = "$s";
outs[5] = skipChr(seqName);
ins[6] = "$D";
outs[6] = db;
ins[7] = "$P";  /* for an item name of the form:  prefix:suffix */
ins[8] = "$p";	/* the P is the prefix, the p is the suffix */
if (stringIn(":", idInUrl)) {
    char *itemClone = cloneString(idInUrl);
    char *suffix = stringIn(":", itemClone);
    char *suffixClone = cloneString(suffix+1); /* +1 skip the : */
    char *nextColon = stringIn(":", suffixClone+1);
    if (nextColon)	/* terminate suffixClone suffix */
        *nextColon = '\0';	/* when next colon is present */
    *suffix = '\0';   /* terminate itemClone prefix */
    outs[7] = itemClone;
    outs[8] = suffixClone;
    /* small memory leak here for these cloned strings */
    /* not important for a one-time operation in a CGI that will exit */
} else {
    outs[7] = idInUrl;	/* otherwise, these are not expected */
    outs[8] = idInUrl;	/* to be used */
}

// URL may now contain item boundaries
ins[9] = "${";
ins[10] = "$}";
ins[13] = "$#";
if (cart!=NULL && cartOptionalString(cart, "o") && cartOptionalString(cart, "t"))
    {
    char *itemBeg = cartString(cart, "o"); // unexpected commas?
    char *itemEnd = cartString(cart, "t");
    outs[9] = itemBeg;
    outs[10] = itemEnd;
    safef(oneBasedStart, sizeof(oneBasedStart), "%d", cartInt(cart, "o") + 1);
    outs[13] = oneBasedStart;
    }
else // should never be but I am unwilling to bet the farm
    {
    outs[9] = startString;
    outs[10] = endString;
    safef(oneBasedStart, sizeof(oneBasedStart), "%d", winStart + 1);
    outs[13] = oneBasedStart;
    }

ins[11] = "$n";
outs[11] = scName;

ins[12] = "$taxId";
outs[12] = taxId;

uUrl = subMulti(url, ArraySize(ins), ins, outs);
outs[0] = eItem;
eUrl = subMulti(url, ArraySize(ins), ins, outs);
dyStringFree(&uUrl);
freeMem(eItem);
freeMem(scName);

// substitute $<fieldName> variables
if (!fields)
    return eUrl->string;

int fieldCount = slCount(fields);
char **fieldNames = NULL, **fieldVals = NULL;
AllocArray(fieldNames, fieldCount);
AllocArray(fieldVals, fieldCount);
int i;
struct slPair *field;
for (i=0, field=fields; i<fieldCount; i++, field=field->next)
    {
    char buf[64];
    safef(buf, sizeof buf, "$<%s>", field->name);
    fieldNames[i] = cloneString(buf);
    fieldVals[i] = (char *)field->val;
    }
struct dyString *fUrl = subMulti(eUrl->string, fieldCount, fieldNames, fieldVals);
    return fUrl->string;
}

char *checkDataVersion(char *database, struct trackDb *tdb)
/* see if trackDb has a dataVersion setting and check that file for version */
{
// try the metadata
metadataForTable(database, tdb, NULL);
char *version = (char *)metadataFindValue(tdb, "dataVersion");

// try trackDb itself, this automatically will go up the hierarchy
if (version == NULL)
    version = trackDbSetting(tdb, "dataVersion");

if (version != NULL)
    {
    // dataVersion can also be the path to a local file, for otto tracks
    if (!trackHubDatabase(database) && !isHubTrack(tdb->table) && startsWith("/", version))
        {
        char *path = replaceInUrl(version, "", NULL, database, "", 0, 0, tdb->track, FALSE, NULL);
        struct lineFile* lf = lineFileMayOpen(path, TRUE);
        if (lf)
            version = lineFileReadAll(lf);
        else
            version = NULL;
        lineFileClose(&lf);
        }
    }
return version;
}

void printDataVersion(char *database, struct trackDb *tdb)
/* If this annotation has a dataVersion setting, print it.
 * check hgFixed.trackVersion, meta data and trackDb 'dataVersion'. */
{
char *version = checkDataVersion(database, tdb);

if (version == NULL)
    {
    // try the hgFixed.trackVersion table
    struct trackVersion *trackVersion = getTrackVersion(database, tdb->track);
    // try trackVersion table with parent, for composites/superTracks
    if (trackVersion == NULL && tdb->parent != NULL)
        trackVersion = getTrackVersion(database, tdb->parent->track);
    if (trackVersion != NULL)
        version = trackVersion->version;
    }

if (isNotEmpty(version))
    printf("<B>Source data version:</B> %s <BR>\n", version);
}

void printRelatedTracks(char *database, struct hash *trackHash, struct trackDb *tdb, struct cart *cart)
/* Maybe print a "related track" section */
{
if (trackHubDatabase(database))
    return;
char *relatedTrackTable = cfgOptionDefault("db.relatedTrack","relatedTrack");
struct sqlConnection *conn = hAllocConn(database);
if (!sqlTableExists(conn, relatedTrackTable))
    {
    hFreeConn(&conn);
    return;
    }

char query[256];
sqlSafef(query, sizeof(query),
    "select track2, why from %s where track1='%s'", relatedTrackTable, tdb->track);

char **row;
struct sqlResult *sr;
sr = sqlGetResult(conn, query);
row = sqlNextRow(sr);
if (row != NULL)
    {
    puts("<b>Related tracks</b>\n");
    puts("<ul>\n");
    struct hash *otherTracksAndDesc = hashNew(0);
    char *why, *otherTrack;
    for (; row != NULL; row = sqlNextRow(sr))
        {
        otherTrack = row[0];
        why = row[1];
        // hopefully relatedTracks.ra doesn't have dupes but hash them just in case
        hashReplace(otherTracksAndDesc, cloneString(otherTrack), cloneString(why));
        }

    struct hashEl *hel, *helList = hashElListHash(otherTracksAndDesc);
    for (hel = helList; hel != NULL; hel = hel->next)
        {
        char *otherTrack = (char *)hel->name;
        char *why = (char *)hel->val;
        struct trackDb *otherTdb = hashFindVal(trackHash, otherTrack);
        // super tracks are not in the hash:
        if (!otherTdb)
            otherTdb = tdbForTrack(database, otherTrack, NULL);
        if (otherTdb)
            {
            puts("<li>");
            printf("<a href=\"%s?g=%s&%s\">%s</a>", hTrackUiForTrack(otherTdb->track), otherTdb->track, cartSidUrlString(cart), otherTdb->shortLabel);
            puts(": ");
            puts(why);
            }
        }
        puts("</ul>\n");
    }
sqlFreeResult(&sr);
hFreeConn(&conn);
}
