83858083785a6b2b1f3c05f93e818e5a89318692 max Tue Aug 25 01:42:34 2026 -0700 hgSession: show cytoBand and gene/locus annotations for each saved session; date-only Created/Last used columns with full timestamp on hover. Factor locusName lookup into reusable hLocusName/hLocusNameExpand in hdb.c and reuse from hgBlat. refs #38157 The Assembly column now shows band, gene/locus name and Mbp position (separated by whitespace, normal text size). hLocusNameExpand centralizes the ex:/in:/ig: and | to - expansion that hgBlat's results page did inline. diff --git src/hg/hgSession/hgSession.c src/hg/hgSession/hgSession.c index c63fcc0bd8e..683bfe1ad64 100644 --- src/hg/hgSession/hgSession.c +++ src/hg/hgSession/hgSession.c @@ -2172,84 +2172,168 @@ if (sqlTableExists(conn, namedSessionTable)) { char *encUserName = cgiEncodeFull(userName); boolean gotSettings = (sqlFieldIndex(conn, namedSessionTable, "settings") >= 0); char query[512]; if (gotSettings) sqlSafef(query, sizeof(query), "SELECT sessionName, shared, firstUse, useCount, contents, settings, lastUse FROM %s " "WHERE userName = '%s' ORDER BY sessionName;", namedSessionTable, encUserName); else sqlSafef(query, sizeof(query), "SELECT sessionName, shared, firstUse, useCount, contents, lastUse FROM %s " "WHERE userName = '%s' ORDER BY sessionName;", namedSessionTable, encUserName); struct sqlResult *sr = sqlGetResult(conn, query); char **row; + /* Cache one connection per assembly db so the per-session band/locus lookups don't + * re-open a connection for every row when many sessions share an assembly. */ + struct hash *dbConnCache = hashNew(0); while ((row = sqlNextRow(sr)) != NULL) { char *encSessionName = row[0]; char *sessionName = cgiDecodeClone(encSessionName); int shared = atoi(row[1]); char *firstUse = cloneString(row[2]); struct tm firstUseTm; ZeroVar(&firstUseTm); strptime(firstUse, "%Y-%m-%d %T", &firstUseTm); long epoch = (long)mktime(&firstUseTm); + /* created = date only for display; createdFull = date+minute for the hover. */ char *dateOnly = cloneString(firstUse); char *spacePt = strchr(dateOnly, ' '); if (spacePt != NULL) *spacePt = '\0'; + char *createdFull = cloneString(firstUse); + if (strlen(createdFull) == 19) + createdFull[16] = '\0'; char *db2 = sessionValFromContents(row[4], "db"); char *pos2 = sessionValFromContents(row[4], "position"); char *description = gotSettings ? getSetting(row[5], "description") : NULL; /* lastUse (last time the session was saved/overwritten/loaded) drives the "most * recently saved session" quick-update shortcut on the client. */ char *lastUse = cloneString(row[gotSettings ? 6 : 5]); struct tm lastUseTm; ZeroVar(&lastUseTm); strptime(lastUse, "%Y-%m-%d %T", &lastUseTm); long lastUseEpoch = (long)mktime(&lastUseTm); - /* Trim the seconds off the display: "2026-08-21 10:29:25" -> "2026-08-21 10:29". */ + char *lastUseDate = cloneString(lastUse); /* date only for display */ + char *sp2 = strchr(lastUseDate, ' '); + if (sp2 != NULL) + *sp2 = '\0'; + /* Trim the seconds off the full value shown on hover: "...10:29:25" -> "...10:29". */ if (strlen(lastUse) == 19) lastUse[16] = '\0'; + + /* Band (from cytoBand) and locus (from locusName) for the saved position, looked up in + * the session's own assembly. Skips hub assemblies and missing tables. For a very + * large region there are too many genes to name, so say so instead. */ + char *band = NULL, *locus = NULL; + if (isNotEmpty(db2) && isNotEmpty(pos2) && !trackHubDatabase(db2)) + { + char *posClone = cloneString(pos2); + stripChar(posClone, ','); + char *colon = strrchr(posClone, ':'); + char *chrom = NULL; + int start = 0, end = 0; + boolean parsed = FALSE; + if (colon != NULL) + { + *colon = '\0'; + chrom = posClone; + char *dash = strchr(colon + 1, '-'); + if (dash != NULL) + { + *dash = '\0'; + start = atoi(colon + 1) - 1; /* display is 1-based; tables are 0-based */ + if (start < 0) + start = 0; + end = atoi(dash + 1); + parsed = (end > start); + } + } + if (parsed) + { + struct sqlConnection *dbConn = hashFindVal(dbConnCache, db2); + if (dbConn == NULL && sqlDatabaseExists(db2)) + { + dbConn = hAllocConn(db2); + hashAdd(dbConnCache, db2, dbConn); + } + if (dbConn != NULL) + { + if (sqlTableExists(dbConn, "cytoBand")) + { + char bandBuf[HDB_MAX_BAND_STRING]; + if (hChromBandConn(dbConn, chrom, start, bandBuf) && bandBuf[0]) + band = cloneString(bandBuf); + } + if ((end - start) > 10000000) + locus = cloneString("Too large for the genes"); + else + locus = hLocusName(dbConn, chrom, start, end); + } + } + freez(&posClone); + } + struct dyString *dyUrl = dyStringNew(0); addSessionLink(dyUrl, encUserName, encSessionName, FALSE, TRUE); jsonWriteObjectStart(jw, NULL); jsonWriteString(jw, "name", sessionName); jsonWriteString(jw, "encName", encSessionName); jsonWriteNumber(jw, "shared", shared); jsonWriteString(jw, "created", dateOnly); + jsonWriteString(jw, "createdFull", createdFull); jsonWriteNumber(jw, "createdEpoch", epoch); jsonWriteString(jw, "lastUse", lastUse); + jsonWriteString(jw, "lastUseDate", lastUseDate); jsonWriteNumber(jw, "lastUseEpoch", lastUseEpoch); jsonWriteNumber(jw, "useCount", atoll(row[3])); if (isNotEmpty(db2)) jsonWriteString(jw, "db", trackHubSkipHubName(db2)); if (isNotEmpty(pos2)) jsonWriteString(jw, "position", pos2); + if (isNotEmpty(band)) + jsonWriteString(jw, "band", band); + if (isNotEmpty(locus)) + jsonWriteString(jw, "locus", locus); if (isNotEmpty(description)) jsonWriteString(jw, "description", description); jsonWriteString(jw, "shareUrl", dyUrl->string); jsonWriteObjectEnd(jw); dyStringFree(&dyUrl); + freez(&band); + freez(&locus); freez(&firstUse); freez(&dateOnly); + freez(&createdFull); + freez(&lastUse); + freez(&lastUseDate); freez(&sessionName); } sqlFreeResult(&sr); + /* Release the cached per-assembly connections. */ + struct hashEl *hel, *helList = hashElListHash(dbConnCache); + for (hel = helList; hel != NULL; hel = hel->next) + { + struct sqlConnection *dbConn = hel->val; + hFreeConn(&dbConn); + } + hashElFreeList(&helList); + hashFree(&dbConnCache); } hDisconnectCentral(&conn); } jsonWriteListEnd(jw); // sessions } void doMainPageNew(char *userName, char *message) /* Render the experimental client-rendered Sessions page: framework header (gold "My Sessions" * band), the experimental banner, an empty #sessionApp container, and the hgSessionData JSON that * hgSession.js reads to build the UI. */ { cspWriteResponseHeader(); puts("Content-Type:text/html\n"); cartWebStart(cart, NULL, "My Sessions"); jsInit();