403ab7c9c2b204f487bb2f86260ffdab355e9517 jcasper Wed Aug 19 05:49:25 2026 -0700 Faceted composites should apply the active sort order to the tracks being displayed; changing the sort changes the display order. We also preserve that order when returning to the page. refs #36320 diff --git src/hg/cartDump/cartDump.c src/hg/cartDump/cartDump.c index f959fb22f23..af5b1e9ddc3 100644 --- src/hg/cartDump/cartDump.c +++ src/hg/cartDump/cartDump.c @@ -1,266 +1,327 @@ /* cartDump - Dump contents of cart. */ /* 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 "linefile.h" #include "hash.h" #include "cheapcgi.h" #include "cart.h" #include "hdb.h" #include "jsHelper.h" #include "hui.h" #include "botDelay.h" /* for earlyBotCheck() function at the beginning of main() */ #define delayFraction 1.0 /* standard penalty for most CGIs */ static boolean issueBotWarning = FALSE; #define CART_DUMP_REMOVE_VAR "n/a" struct hash *oldVars = NULL; void handleFacets(struct cart *cart) /* Process track vis updates for a faceted composite. This uses information about * which data types (dt) and data elements (de) were on before and after the * user made UI changes. If data types are used by the track, they're usually * things like "reads" and "signals". Data elements are generally either sample * IDs (if data types are also used) or just the ends of track names. * * If the user adds a new data element, we turn on all associated tracks for the * active data types. If a user adds a new data type, we turn on all associated * tracks for the active data elements. But if a data element/type combo was already * checked, we don't re-enable that track because the user might have hidden it * manually. */ { char *mName = "cartDump.metaDataId"; if (!cgiVarExists(mName)) return; char *mdid = cgiOptionalString(mName); char mdid_de_was[1024], mdid_de_now[1024], mdid_dt_was[1024], mdid_dt_now[1024]; +char mdid_sort[1024]; safef(mdid_de_was, sizeof(mdid_de_was), "%s.de_was", mdid); safef(mdid_de_now, sizeof(mdid_de_now), "%s.de_now", mdid); safef(mdid_dt_was, sizeof(mdid_dt_was), "%s.dt_was", mdid); safef(mdid_dt_now, sizeof(mdid_dt_now), "%s.dt_now", mdid); +safef(mdid_sort, sizeof(mdid_sort), "%s.facetSortOrder", mdid); // Grab the lists of which de/dt elements were on before and after the user // changed settings around struct slName *de_was_list = cgiStringList(mdid_de_was); struct slName *de_now_list = cgiStringList(mdid_de_now); struct slName *dt_was_list = cgiStringList(mdid_dt_was); struct slName *dt_now_list = cgiStringList(mdid_dt_now); // For faster lookup struct hash *de_was_hash = hashFromSlNameList(de_was_list); struct hash *de_now_hash = hashFromSlNameList(de_now_list); struct hash *dt_was_hash = hashFromSlNameList(dt_was_list); struct hash *dt_now_hash = hashFromSlNameList(dt_now_list); // Check if we sent dt_was/now variables, indicating that this composite has data types boolean hasDataTypes = (dt_was_list != NULL || dt_now_list != NULL); char subtrackSetting[1024]; +char prioritySetting[1024]; + +// Remember how the faceted table was sorted, so that the next visit to the track +// UI page comes back in the same order. The value is a list of metadata field +// names with directions ("field=+ field2=-"), mirroring the classic composite +// ".sortOrder" cart variable. It's opaque here - only the javascript can +// turn field names into an ordering, since only it reads the metadata file. +char *facetSortOrder = cgiOptionalString(mdid_sort); +if (isNotEmpty(facetSortOrder)) + cartSetString(cart, mdid_sort, facetSortOrder); +else + cartRemove(cart, mdid_sort); + +// Any ".priority" values left over from an earlier submission are stale, since the +// loops below assign a fresh, dense 1..N to exactly the subtracks that are on now. +// Clearing them first means the subtracks the user just turned off don't keep a +// priority that would place them oddly if they're ever turned on again elsewhere. +// Note that "_*" can't match the composite's own ".priority" - the +// subtrack names are joined with an underscore rather than a dot. +char priorityWild[1024]; +safef(priorityWild, sizeof(priorityWild), "%s_*.priority", mdid); +cartRemoveLike(cart, priorityWild); + +// A subtrack the user has dragged up or down in the hgTracks image carries an +// "_imgOrd" value, and flatTracksCmp() in hgTracks/imageV2.c sorts on that before +// it ever looks at priority - so without this the dragged position would win and +// the sort established here would appear to be ignored. cartJustify() in cart.c +// normally clears these whenever a ".priority" arrives as a CGI variable, but ours +// are set with cartSetInt() below, well after cartJustify() has run for this +// request, so we have to do it ourselves. This is what the wiggle sort in +// hgTracks.c does when it establishes a new order for a composite. +char imgOrdWild[1024]; +safef(imgOrdWild, sizeof(imgOrdWild), "%s_*_imgOrd", mdid); +cartRemoveLike(cart, imgOrdWild); if (hasDataTypes) { // Cross-product mode: tracks are identified by mdid_de_dt // Turn ON: (de_on x dt_now), (de_now x dt_on) // where de_on = de_now - de_was, dt_on = dt_now - dt_was for (struct slName *de = de_now_list; de != NULL; de = de->next) { boolean de_is_new = (hashLookup(de_was_hash, de->name) == NULL); for (struct slName *dt = dt_now_list; dt != NULL; dt = dt->next) { boolean dt_is_new = (hashLookup(dt_was_hash, dt->name) == NULL); if (de_is_new || dt_is_new) { safef(subtrackSetting, sizeof(subtrackSetting), "%s_%s_%s_sel", mdid, de->name, dt->name); cartSetString(cart, subtrackSetting, "1"); } } } // Turn OFF: (de_off x dt_was), (de_was x dt_off) // where de_off = de_was - de_now, dt_off = dt_was - dt_now for (struct slName *de = de_was_list; de != NULL; de = de->next) { boolean de_is_off = (hashLookup(de_now_hash, de->name) == NULL); for (struct slName *dt = dt_was_list; dt != NULL; dt = dt->next) { boolean dt_is_off = (hashLookup(dt_now_hash, dt->name) == NULL); if (de_is_off || dt_is_off) { safef(subtrackSetting, sizeof(subtrackSetting), "%s_%s_%s_sel", mdid, de->name, dt->name); cartSetString(cart, subtrackSetting, "0"); } } } + // Set each shown subtrack's priority to match the order the data elements + // are currently sorted in the faceted table (de_now arrives in that order). + // Data elements are the outer loop so a sample's data-type subtracks stay + // contiguous, in the sample's sorted position. + int priority = 0; + for (struct slName *de = de_now_list; de != NULL; de = de->next) + { + for (struct slName *dt = dt_now_list; dt != NULL; dt = dt->next) + { + safef(prioritySetting, sizeof(prioritySetting), + "%s_%s_%s.priority", mdid, de->name, dt->name); + cartSetInt(cart, prioritySetting, ++priority); + } + } + } else { // Data elements only mode: tracks are identified by mdid_de // Turn ON: de_now - de_was for (struct slName *de = de_now_list; de != NULL; de = de->next) { if (hashLookup(de_was_hash, de->name) == NULL) { safef(subtrackSetting, sizeof(subtrackSetting), "%s_%s_sel", mdid, de->name); cartSetString(cart, subtrackSetting, "1"); } } // Turn OFF: de_was - de_now for (struct slName *de = de_was_list; de != NULL; de = de->next) { if (hashLookup(de_now_hash, de->name) == NULL) { safef(subtrackSetting, sizeof(subtrackSetting), "%s_%s_sel", mdid, de->name); cartSetString(cart, subtrackSetting, "0"); } } + // Set each shown subtrack's priority to match the order the data elements + // are currently sorted in the faceted table (de_now arrives in that order). + int priority = 0; + for (struct slName *de = de_now_list; de != NULL; de = de->next) + { + safef(prioritySetting, sizeof(prioritySetting), + "%s_%s.priority", mdid, de->name); + cartSetInt(cart, prioritySetting, ++priority); + } + } hashFree(&de_was_hash); hashFree(&de_now_hash); hashFree(&dt_was_hash); hashFree(&dt_now_hash); slFreeList(&de_was_list); slFreeList(&de_now_list); slFreeList(&dt_was_list); slFreeList(&dt_now_list); cartRemove(cart, mName); cartRemove(cart, mdid_de_was); cartRemove(cart, mdid_de_now); cartRemove(cart, mdid_dt_was); cartRemove(cart, mdid_dt_now); } void doMiddle(struct cart *cart) /* cartDump - Dump contents of cart. */ { #define MATCH_VAR "match" char *vName = "cartDump.varName"; char *vVal = "cartDump.newValue"; char *wildcard; boolean asTable = cartVarExists(cart,CART_DUMP_AS_TABLE); if (cgiVarExists("submit")) { char *varName = cgiOptionalString(vName); char *newValue = cgiOptionalString(vVal); if (isNotEmpty(varName) && isNotEmpty(newValue)) { varName = skipLeadingSpaces(varName); eraseTrailingSpaces(varName); if (sameString(newValue, CART_DUMP_REMOVE_VAR) || sameString(newValue, CART_VAR_EMPTY)) cartRemove(cart, varName); else cartSetString(cart, varName, newValue); } cartRemove(cart, vVal); cartRemove(cart, "submit"); } if (cgiVarExists("noDisplay")) { // update cart vars for a track, called by hgTracks.js and ajax.js // not useful to hackers, so there is no need to call bottleneck. char *trackName = cgiOptionalString("g"); if (trackName != NULL && hashNumEntries(oldVars) > 0) { char *db = cartString(cart, "db"); struct trackDb *tdb = hTrackDbForTrack(db, trackName); if (tdb != NULL && tdbIsComposite(tdb)) { struct lm *lm = lmInit(0); cartTdbTreeCleanupOverrides(tdb,cart,oldVars,lm); lmCleanup(&lm); } } handleFacets(cart); return; } // To discourage hacking, call bottleneck if (issueBotWarning) { char *ip = getenv("REMOTE_ADDR"); botDelayMessage(ip, botDelayMillis); } if (asTable) { jsIncludeFile("jquery.js",NULL); // required by utils.js jsIncludeFile("utils.js",NULL); jsIncludeFile("ajax.js",NULL); printf("Show as plain text.
",CART_DUMP_AS_TABLE); printf("
\n"); cartSaveSession(cart); printf("Variables can be altered by changing the values and then leaving the field (onchange event will use ajax).\n"); printf("Enter %s or %s to remove a variable.", COLOR_DARKBLUE,CART_DUMP_REMOVE_VAR,COLOR_DARKBLUE,CART_VAR_EMPTY); printf("
Add a variable named: "); cgiMakeTextVar(vName, "", 12); printf(" value: "); cgiMakeTextVar(vVal, "", 24); printf(" "); cgiMakeButton("submit", "refresh"); // Says refresh but works as a submit. printf("      " "\n", COLOR_RED); printf("
\n"); } else { printf("Show as updatable table.
",CART_DUMP_AS_TABLE); } printf("
");
 wildcard = cgiOptionalString(MATCH_VAR);
 if (wildcard)
     cartDumpLike(cart, wildcard);
 else
     cartDump(cart);
 printf("
"); if (!asTable) { printf("
\n"); cartSaveSession(cart); printf("Add/alter a variable named: "); cgiMakeTextVar(vName, cartUsualString(cart, vName, ""), 12); printf(" new value "); cgiMakeTextVar(vVal, "", 24); printf(" "); cgiMakeButton("submit", "submit"); printf("
Put %s in for the new value to clear a variable.", COLOR_DARKBLUE,CART_DUMP_REMOVE_VAR); printf("
\n"); } printf("

Cookies passed to %s:
\n%s\n

\n", cgiServerNamePort(), getenv("HTTP_COOKIE")); } char *excludeVars[] = { "submit", "Submit", "noDisplay", MATCH_VAR, NULL }; int main(int argc, char *argv[]) /* Process command line. */ { long enteredMainTime = clock1000(); /* 0, 0, == use default 10 second for warning, 20 second for immediate exit */ issueBotWarning = earlyBotCheck(enteredMainTime, "cartDump", delayFraction, 0, 0, "html"); cgiSpoof(&argc, argv); oldVars = hashNew(10); cartHtmlShell("Cart Dump", doMiddle, hUserCookie(), excludeVars, oldVars); cgiExitTime("cartDump", enteredMainTime); return 0; }