7d149b1935f8a1e5f11f31f3cb8da99d31b3a8e6 jcasper Fri Apr 10 16:34:48 2026 -0700 Replaced handrolled JSON structure in faceted composite output from hgTrackUi with jsonWrite.h library-generated version, and added a defaultSortField trackDb setting to faceted composites to improve the subtrack presentation order, refs #36320 diff --git src/hg/hgTrackUi/hgTrackUi.c src/hg/hgTrackUi/hgTrackUi.c index 46eb5b6070e..ab13cb88f3e 100644 --- src/hg/hgTrackUi/hgTrackUi.c +++ src/hg/hgTrackUi/hgTrackUi.c @@ -45,30 +45,31 @@ #include "vcfUi.h" #include "bbiFile.h" #include "ensFace.h" #include "microarray.h" #include "trackVersion.h" #include "gtexUi.h" #include "genbank.h" #include "botDelay.h" #include "customComposite.h" #include "hicUi.h" #include "decoratorUi.h" #include "genark.h" #include "cart.h" #include "filePath.h" #include "md5.h" +#include "jsonWrite.h" #ifdef USE_HAL #include "halBlockViz.h" #endif #define MAIN_FORM "mainForm" #define WIGGLE_HELP_PAGE "../goldenPath/help/hgWiggleTrackHelp.html" /* for earlyBotCheck() function at the beginning of main() */ #define delayFraction 0.25 /* standard penalty is 1.0 for most CGIs */ /* this one is 0.25 */ static boolean issueBotWarning = FALSE; struct cart *cart = NULL; /* Cookie cart with UI settings */ char *database = NULL; /* Current database. */ @@ -3002,31 +3003,30 @@ char *dtParts[2]; int partCount = chopByCharRespectDoubleQuotes(datatypes[i], '|', dtParts, 2); char *name, *title; name = title = dtParts[0]; if (partCount > 1) title = dtParts[1]; slPairAdd(&list, name, cloneString(title)); } freeMem(tdbDataTypes); return list; } unsigned int cartDbParseId(char *, char **); // ADS: avoid extra include -#define COMMA_IF(x) (((x)++) ? "," : "") // ADS: pattern for JSON comma static void facetedCompositeUi(struct trackDb *tdb) { /* ADS: How facetedComposite differs from other track types * - compositeTrack track setting is "faceted" * - Required fields in the 'settings' longblob for the trackDb entry: * - 'metaDataUrl': a non-blocked URL (can be server-local) with * metadata to generate the table. This might change to an existing metadata * setting in the future. * - 'dataTypes': the names of the data types, ordered and space separated. * * This function will embed sessionDb.settings/cart data in the * generated HTML. Instead of embedding all relevant tracks, it * parses the tracks named like: * @@ -3036,36 +3036,30 @@ * much smaller. The faceted composite assumes that selection of * dataTypes applies to all dataElements, but within the cart, these * are separate tracks, and each must be present to be drawn. But * the JS doesn't need the product {dataType} x {dataElement}, just * the union {datType} U {dataElement}. These are put in two arrays * in a JSON section of the HTML. */ const int token_size = 1024; // html elements for the controls page (from singleCellMerged) const char pageStyle[] = ""; const char placeholderDiv[] = "
\n"; -const char openJSON[] = "\n"; -const char openDataTypesJSON[] = "\"dataTypes\":{"; -const char closeDataTypesJSON[] = "}"; // closing a dict -const char openDataElementsJSON[] = "\"dataElements\":["; -const char closeDataElementsJSON[] = "]"; // closing an array // --- Get data from 'settings' field in 'trackDb' entry --- // required const char *metaDataUrl = trackDbSetting(tdb, "metaDataUrl"); const char *primaryKey = trackDbSetting(tdb, "primaryKey"); struct slPair *dataTypes = parseDataTypes(tdb); boolean hasDataTypes = (dataTypes != NULL); // optional const char *colorSettingsUrl = (const char *)hashFindVal(tdb->settingsHash, "colorSettingsUrl"); const char *maxCheckboxes = (const char *)hashFindVal(tdb->settingsHash, "maxCheckboxes"); // --- done parsing values from trackDb.settings --- const char *metaDataId = tdb->track; @@ -3087,34 +3081,35 @@ if (chopLine(clone = cloneString(setting), words) >= 2) if (sameString(words[1], "off")) enabled = FALSE; freeMem(clone); } if (enabled) { char val[1024]; safef(val, sizeof(val), "%s_sel", st->track); if (!cartVarExists(cart, val)) hashAdd(defaultOn, val, NULL); } } /* --- START embedded JSON data --- */ -printf(openJSON); -printf(openDataTypesJSON); +struct jsonWrite *jw = jsonWriteNew(); +jsonWriteObjectStart(jw, NULL); + // find selected data types -int not_first = 0; +jsonWriteObjectStart(jw, "dataTypes"); struct slName *selectedDataTypes = NULL; // non-null val will be used as flag if (hasDataTypes) { for (struct slPair *thisType = dataTypes; thisType != NULL; thisType = thisType->next) { char toMatch[token_size]; boolean selected = FALSE; safef(toMatch, token_size, "%s_*_%s_sel", metaDataId, thisType->name); // Easy case - check if there's a defaultOn track still active with this track type if (hashItemExistsLike(defaultOn, toMatch)) { slNameAddHead(&selectedDataTypes, thisType->name); selected = TRUE; } else @@ -3123,129 +3118,140 @@ struct slPair *dt_vars = cartVarsLike(cart, toMatch); struct slPair *this_var = dt_vars; while (this_var != NULL) { if (cartBoolean(cart, this_var->name)) { slNameAddHead(&selectedDataTypes, thisType->name); selected = TRUE; break; } this_var = this_var->next; } slPairFreeList(&dt_vars); } char *label = htmlEncode(stripEnclosingDoubleQuotes(thisType->val)); - printf("%s\"%s\": {\"active\":%d, \"title\":\"%s\"}", COMMA_IF(not_first), thisType->name, - selected ? 1 : 0, label); + jsonWriteObjectStart(jw, thisType->name); + jsonWriteNumber(jw, "active", selected ? 1 : 0); + jsonWriteString(jw, "title", label); + jsonWriteObjectEnd(jw); freeMem(label); } } // else: dataTypes dict is empty - JS will detect this -printf(closeDataTypesJSON); -printf(","); // add separator +jsonWriteObjectEnd(jw); // find selected data sets -printf(openDataElementsJSON); -not_first = 0; +jsonWriteListStart(jw, "dataElements"); if (hasDataTypes) { char toMatch[token_size]; safef(toMatch, token_size, "%s_*_*_sel", metaDataId); struct slPair *mdidVars = cartVarsLike(cart, toMatch); for (struct slPair *le = mdidVars; le != NULL; le = le->next) { if (cartBoolean(cart, le->name)) { const char *nameStart = le->name + metaDataIdLen + 1; const char *nameEnd = strchr(nameStart, '_'); if (nameEnd && nameEnd > nameStart) { - const int nameLen = nameEnd - nameStart; - printf("%s\"%.*s\"", COMMA_IF(not_first), nameLen, nameStart); + char *name = cloneStringZ(nameStart, nameEnd - nameStart); + jsonWriteString(jw, NULL, name); + freeMem(name); } } } slPairFreeList(&mdidVars); // Now add data elements on by default that haven't been modified struct hashEl *el, *elList = hashElListHash(defaultOn); for (el = elList; el != NULL; el = el->next) { if (!cartVarExistsLike(cart, el->name)) { const char *nameStart = el->name + metaDataIdLen + 1; const char *nameEnd = strchr(nameStart, '_'); if (nameEnd && nameEnd > nameStart) { - const int nameLen = nameEnd - nameStart; - printf("%s\"%.*s\"", COMMA_IF(not_first), nameLen, nameStart); + char *name = cloneStringZ(nameStart, nameEnd - nameStart); + jsonWriteString(jw, NULL, name); + freeMem(name); } } } hashElFreeList(&elList); } else { // No data types - look for {mdid}_{de}_sel pattern (no dataType component) char toMatch[token_size]; safef(toMatch, token_size, "%s_*_sel", metaDataId); struct slPair *mdidVars = cartVarsLike(cart, toMatch); for (struct slPair *le = mdidVars; le != NULL; le = le->next) { if (cartBoolean(cart, le->name)) { // Extract data element name: between mdid_ and _sel const char *nameStart = le->name + metaDataIdLen + 1; const char *nameEnd = strstr(nameStart, "_sel"); if (nameEnd && nameEnd > nameStart) { - const int nameLen = nameEnd - nameStart; - printf("%s\"%.*s\"", COMMA_IF(not_first), nameLen, nameStart); + char *name = cloneStringZ(nameStart, nameEnd - nameStart); + jsonWriteString(jw, NULL, name); + freeMem(name); } } } slPairFreeList(&mdidVars); // Now add data elements on by default that haven't been modified struct hashEl *el, *elList = hashElListHash(defaultOn); for (el = elList; el != NULL; el = el->next) { if (!cartVarExistsLike(cart, el->name)) { const char *nameStart = el->name + metaDataIdLen + 1; const char *nameEnd = strstr(nameStart, "_sel"); if (nameEnd && nameEnd > nameStart) { - const int nameLen = nameEnd - nameStart; - printf("%s\"%.*s\"", COMMA_IF(not_first), nameLen, nameStart); + char *name = cloneStringZ(nameStart, nameEnd - nameStart); + jsonWriteString(jw, NULL, name); + freeMem(name); } } } hashElFreeList(&elList); } -printf(closeDataElementsJSON); -printf(",\"mdid\": \"%s\"", metaDataId); -printf(",\"primaryKey\": \"%s\"", primaryKey); // must exist +jsonWriteListEnd(jw); + +jsonWriteString(jw, "mdid", (char *)metaDataId); +jsonWriteString(jw, "primaryKey", (char *)primaryKey); // must exist if (maxCheckboxes) // only if present in trackDb.settings entry - printf(",\"maxCheckboxes\": \"%s\"", maxCheckboxes); + jsonWriteString(jw, "maxCheckboxes", (char *)maxCheckboxes); if (colorSettingsUrl) // only if present in trackDb.settings entry - printf(",\"colorSettingsUrl\": \"%s\"", cgiEncode((char*) colorSettingsUrl)); -printf(",\"metadataUrl\": \"%s\"", cgiEncode((char*) metaDataUrl)); -printf(",\"track\": \"%s\"", tdb->track); + jsonWriteString(jw, "colorSettingsUrl", cgiEncode((char *)colorSettingsUrl)); +jsonWriteString(jw, "metadataUrl", cgiEncode((char *)metaDataUrl)); +jsonWriteString(jw, "track", tdb->track); +char *defaultSortField = trackDbSetting(tdb, "defaultSortField"); +if (isNotEmpty(defaultSortField)) + jsonWriteString(jw, "defaultSortField", defaultSortField); if (isNotEmpty(cartOptionalString(cart, "udcTimeout"))) - printf(",\"udcTimeout\": true"); -printf(closeJSON); + jsonWriteBoolean(jw, "udcTimeout", TRUE); + +jsonWriteObjectEnd(jw); +printf("\n", jw->dy->string); +jsonWriteFree(&jw); /* --- END embedded JSON data --- */ jsIncludeFile("dataTables-2.2.2.min.js", NULL); jsIncludeFile("dataTables.select-3.0.0.min.js", NULL); jsIncludeFile("facetedComposite.js", NULL); webIncludeResourceFile("dataTables-2.2.2.min.css"); webIncludeResourceFile("dataTables.select-3.0.0.min.css"); webIncludeResourceFile("facetedComposite.css"); // cleanup slPairFreeValsAndList(&dataTypes); hashFree(&defaultOn); }