e6482750f5f9258ac7578e2f458ef81f634721d2
jcasper
  Sun Nov 23 23:15:11 2025 -0800
More kent-ifying of the faceted composite code, refs #36320

diff --git src/hg/hgTrackUi/hgTrackUi.c src/hg/hgTrackUi/hgTrackUi.c
index fea1821b26e..620d8a43375 100644
--- src/hg/hgTrackUi/hgTrackUi.c
+++ src/hg/hgTrackUi/hgTrackUi.c
@@ -2833,111 +2833,62 @@
 char **ancestors;
 AllocArray(ancestors, count);
 count = 0;
 for(sp=speciesList; sp; sp = sp->next)
     {
     ancestors[count] = sp->name;
     count++;
     }
 char *coalescent = cartOptionalString(cart, codeVarName);
 printf("<B>Set Coalescent Ancestor to:</B>");
 cgiMakeDropListFull(codeVarName, ancestors, ancestors,
     count, coalescent, NULL, NULL);
 }
 #endif
 
-static char **parseDataTypes(struct trackDb *tdb, int *out_count)
+static struct slName *parseDataTypes(struct trackDb *tdb)
 {
-// ADS: almost certainly a function already exists to do this??
-/* Get entries 'nDataTypes' and 'dataTypes' from the 'settings'
- * field for the given 'trackDb' entry. 'nDataTypes' is a count, and
+/* Parse the 'dataTypes' trackDb setting into an slName list.
  * 'dataTypes' is a space separated list of words, each indicating a
- * data type. This function returns the array of data types. The
- * caller must free the returned array and each member. If
- * nDataTypes does not match the number of words parsed from
- * 'dataTypes' it is an error. Return value is NULL on error.
+ * data type. Return value is NULL on error.
  */
-const char *n_datatypes_str = (const char *)hashMustFindVal(tdb->settingsHash, "nDataTypes");
-const int n_datatypes = atoi(n_datatypes_str);
-if (n_datatypes <= 0)
+char *tdbDataTypes = cloneString(trackDbSetting(tdb, "dataTypes"));
+if (tdbDataTypes == NULL)
     return NULL;
 
-const char *datatypes_str = (const char *)hashMustFindVal(tdb->settingsHash, "dataTypes");
-if (!datatypes_str)
-    return NULL;
-
-// returned, must be freed
+// A bit awkward to go through chopByWhite into slNameListFromStringArray, but
+// the slNameList family of functions doesn't have a chopByWhite equivalent.
+int n_datatypes = chopByWhiteRespectDoubleQuotes(tdbDataTypes, NULL, 0);
 char **datatypes = calloc(n_datatypes, sizeof(char *));
-if (!datatypes)
-    return NULL;
-
-const char *name_start = datatypes_str;
-int observed_count = 0;
-
-for (int i = 0; i < n_datatypes; ++i)
-    {
-    const char *name_end = strchr(name_start, ' ');
-    if (!name_end)
-        name_end = strchr(name_start, '\0');
-
-    const int name_len = name_end - name_start;
-    if (name_len <= 0)
-        break;  // skip empty segments
-
-    datatypes[i] = calloc(name_len + 1, sizeof(char));
-    if (!datatypes[i])
-        {  // cleanup on failure
-        for (int j = 0; j < i; ++j) free(datatypes[j]);
-        free(datatypes);
-        return NULL;
-        }
-    memcpy(datatypes[i], name_start, name_len);
-    datatypes[i][name_len] = '\0';
-
-    ++observed_count;
-
-    if (*name_end == '\0')
-        break;
-    name_start = name_end + 1;
-    }
-
-if (n_datatypes != observed_count)
-    {  // cleanup on failure
-    for (int i = 0; i < n_datatypes; ++i)
-        free(datatypes[i]);
-    free(datatypes);
-    return NULL;
-    }
-
-if (out_count)
-    *out_count = observed_count;
-return datatypes;
+chopByWhiteRespectDoubleQuotes(tdbDataTypes, datatypes, n_datatypes);
+struct slName *list = slNameListFromStringArray(datatypes, n_datatypes);
+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.
- * - 'nDataTypes': positive integer counting the data types for each sample.
  * - '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:
  *
  *    facetedCompositeName_dataElementName_dataTypeName_sel=1
  *
  * And only sends unique dataElement and dataTypes, which might be
  * 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.
@@ -2948,126 +2899,120 @@
 
 // html elements for the controls page (from singleCellMerged)
 const char pageStyle[] =
     "<style>body.cgi { background: #F0F0F0; }"
     "table.hgInside { background: #FFFFFF; }</style>";
 const char placeholderDiv[] = "<div id='metadata-placeholder'></div>\n";
 const char openJSON[] = "<script id=\"app-data\" type=\"application/json\">{";
 const char closeJSON[] = "}</script>\n";
 const char openDataTypesJSON[] = "\"dataTypes\":{";
 const char closeDataTypesJSON[] = "}";  // closing a dict
 const char openDataElementsJSON[] = "\"dataElements\":[";
 const char closeDataElementsJSON[] = "]";  // closing an array
 const char metadataTableScriptElement[] =
     "<script type='text/javascript' src='/js/facetedComposite.js'></script>\n";
 
-/* ADS: maybe cart should be used below, but I don't know how from here */
 // parse the hgsid as id and sessionKey
 char *hgsid = cartSessionId(cart);
 char *sessionKey = NULL;
 const int id = cartDbParseId(hgsid, &sessionKey);
 if (!sessionKey)
     errAbort("Failed to parse session key from: %s", hgsid);
 
 // --- Get data from 'settings' field in 'trackDb' entry ---
 // required
 const char *metaDataUrl = trackDbSetting(tdb, "metaDataUrl");
-//    (const char *)hashMustFindVal(tdb->settingsHash, "metaDataUrl");
 const char *primaryKey = trackDbSetting(tdb, "primaryKey");
-//    (const char *)hashMustFindVal(tdb->settingsHash, "primaryKey");
 
-int nDataTypes = 0;
-char **dataTypes = parseDataTypes(tdb, &nDataTypes);
+struct slName *dataTypes = parseDataTypes(tdb);
 if (!dataTypes)
     errAbort("Failed to parse data types from faceted composite settings for: %s", tdb->track);
 // 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;
 
 char queryFmt[] = "SELECT contents FROM sessionDb WHERE id='%d' AND sessionKey='%s';";
 char query[query_buff_size];
 sqlSafef(query, query_buff_size, queryFmt, id, sessionKey);
 
 struct sqlConnection *conn = hConnectCentral();
 const char *contents = sqlQuickString(conn, query);
 struct cgiParsedVars *varList = cgiParsedVarsNew((char *)contents);
 
 printf(pageStyle);       // css
 printf(placeholderDiv);  // placholder
 
 /* --- START embedded JSON data --- */
 printf(openJSON);
 printf(openDataTypesJSON);
 // find selected data types
 int not_first = 0;
-int anySelDataType = -1;  // non-neg val will be used as index and flag
-for (int i = 0; i < nDataTypes; ++i)
+struct slName *anySelDataType = NULL;  // non-null val will be used as flag
+for (struct slName *thisType = dataTypes; thisType != NULL; thisType = thisType->next)
     {
     char toMatch[token_size];
-    safef(toMatch, token_size, "_%s_sel", dataTypes[i]);
+    safef(toMatch, token_size, "_%s_sel", thisType->name);
     boolean dataTypeSel = FALSE;
     for (struct cgiVar *le = varList->list; !dataTypeSel && le; le = le->next)
         if (startsWith(metaDataId, le->name) && endsWith(le->name, toMatch))
             dataTypeSel = TRUE;
-    printf("%s\"%s\": %d", COMMA_IF(not_first), dataTypes[i], dataTypeSel ? 1 : 0);
-    anySelDataType = dataTypeSel ? i : anySelDataType;
+    printf("%s\"%s\": %d", COMMA_IF(not_first), thisType->name, dataTypeSel ? 1 : 0);
+    anySelDataType = dataTypeSel ? thisType : anySelDataType;
     }
 printf(closeDataTypesJSON);
 printf(",");  // add separator
 // find selected data sets
 printf(openDataElementsJSON);
 not_first = 0;
-if (anySelDataType >= 0)
+if (anySelDataType != NULL)
     {
     char suffix[token_size];
-    safef(suffix, token_size, "_%s_sel", dataTypes[anySelDataType]);
+    safef(suffix, token_size, "_%s_sel", anySelDataType->name);
     for (struct cgiVar *le = varList->list; le; le = le->next)
         if (startsWith(metaDataId, le->name) && endsWith(le->name, suffix))
             {
             const char *nameStart = strchr(le->name+strlen(metaDataId), '_');
             if (nameStart)
                 {
                 ++nameStart;  // move past '_'
                 const char *nameEnd = strchr(nameStart, '_');
                 if (nameEnd && nameEnd > nameStart)
                     {
                     const int nameLen = nameEnd - nameStart;
                     printf("%s\"%.*s\"", COMMA_IF(not_first), nameLen, nameStart);
                     }
                 }
             }
     }
 printf(closeDataElementsJSON);
 printf(",\"mdid\": \"%s\"", tdb->track);  // metadata id is track name
 printf(",\"primaryKey\": \"%s\"", primaryKey);  // must exist
 if (maxCheckboxes) // only if present in trackDb.settings entry
     printf(",\"maxCheckboxes\": \"%s\"", maxCheckboxes);
 if (colorSettingsUrl) // only if present in trackDb.settings entry
     printf(",\"colorSettingsUrl\": \"%s\"", colorSettingsUrl);
 printf(",\"metadataUrl\": \"%s\"", metaDataUrl);
 printf(closeJSON);
 /* --- END embedded JSON data --- */
 
 printf(metadataTableScriptElement);
 
 // cleanup
-for (int i = 0; i < nDataTypes; ++i)
-    free(dataTypes[i]);
-free(dataTypes);
+slFreeList(&dataTypes);
 cgiParsedVarsFreeList(&varList);
 hDisconnectCentral(&conn);
 }
 
 void specificUi(struct trackDb *tdb, struct trackDb *tdbList, struct customTrack *ct, boolean ajax)
 /* Draw track specific parts of UI. */
 {
 char *track = tdb->track;
 char *db = database;
 char *liftDb = cloneString(trackDbSetting(tdb, "quickLiftDb"));
 if (liftDb != NULL) 
     db = liftDb;
 // Ideally check cfgTypeFromTdb()/cfgByCfgType() first, but with all these special cases already in
 //    place, lets be cautious at this time.
 // NOTE: Developer, please try to use cfgTypeFromTdb()/cfgByCfgType().