abe6cb020492b82545875d59c570d0a3acc51376
jcasper
  Wed Feb 4 14:41:49 2026 -0800
Adjusting faceted composite cart logic to support tracks that are on
by default, refs #36320

diff --git src/hg/cartDump/cartDump.c src/hg/cartDump/cartDump.c
index c74cc4544ea..43fde324e08 100644
--- src/hg/cartDump/cartDump.c
+++ src/hg/cartDump/cartDump.c
@@ -54,75 +54,135 @@
     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);
             }
         }
 
     if (cgiVarExists(mName))
         {
         char *mdid = cgiOptionalString(mName);
-        char mdid_de[1024], mdid_dt[1024];
-        safef(mdid_de, sizeof(mdid_de), "%s.de", mdid);
-        safef(mdid_dt, sizeof(mdid_dt), "%s.dt", mdid);
-        struct slName *de_list = cgiStringList(mdid_de);
-        struct slName *dt_list = cgiStringList(mdid_dt);
-
-        // This component should change.  Erasing settings doesn't preserve any user selections
-        // (like bigWig display changes) and doesn't work at all for tracks that are on by default.
-        char wc_mdid[1024];
-        safef(wc_mdid, sizeof(wc_mdid), "*_%s", mdid);
-        cartRemoveLike(cart, wc_mdid);
-        cartRemovePrefix(cart, mdid);
-
-        // Check for empty-string sentinel: means no data type suffix
-        boolean noDataTypes = (dt_list != NULL && 
-                               dt_list->name[0] == '\0' && 
-                               dt_list->next == NULL);
+        char mdid_de_was[1024], mdid_de_now[1024], mdid_dt_was[1024], mdid_dt_now[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);
+        // 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];
-        for (struct slName *de_itr = de_list; de_itr; de_itr = de_itr->next)
+
+        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)
                 {
-            if (noDataTypes)
+                boolean de_is_new = (hashLookup(de_was_hash, de->name) == NULL);
+                for (struct slName *dt = dt_now_list; dt != NULL; dt = dt->next)
                     {
-                // No data type suffix - use simpler track names
-                safef(subtrackSetting, sizeof(subtrackSetting), "%s_%s_sel", mdid, de_itr->name);
-                cartSetString(cart, subtrackSetting, "1");
-                safef(subtrackSetting, sizeof(subtrackSetting), "%s_%s", mdid, de_itr->name);
+                    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");
+                        }
+                    }
+                }
+
+            }
         else
             {
-                struct slName *dt_itr = NULL;
-                for (dt_itr = dt_list; dt_itr; dt_itr = dt_itr->next)
+            // 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)
                 {
-                    safef(subtrackSetting, sizeof(subtrackSetting), "%s_%s_%s_sel", mdid, de_itr->name, dt_itr->name);
-                    cartSetString(cart, subtrackSetting, "1");
-                    safef(subtrackSetting, sizeof(subtrackSetting), "%s_%s_%s", mdid, de_itr->name, dt_itr->name);
+                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");
+                    }
+                }
+
             }
 
+        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);
-        cartRemove(cart, mdid_dt);
+        cartRemove(cart, mdid_de_was);
+        cartRemove(cart, mdid_de_now);
+        cartRemove(cart, mdid_dt_was);
+        cartRemove(cart, mdid_dt_now);
         }
 
     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