6cbe668f98ab263238542b8fd6bbf860db113930 angie Wed Jan 28 09:55:13 2015 -0800 Several major changes in the way data sources are represented: 1. Instead of the CGI sending menus for group, track and table, make it send the whole tree of groups, tracks, views, subtracks -- "groupedTrackDb". Now the client-side model can make menus that reflect the tree structure. This also gives us flexibility to tweak labels easily. 2. Instead of hardcoding group, track and table into elements of hgai_querySpec.dataSources, make each data source a proper object with a trackPath (path through groupedTrackDb), and for the UI, other attributes like label and URL for hgTables schema page. When the CGI unpacks hgai_querySpec.dataSources, it now uses trackPath and gets table from the leaf track or subtrack (the last item in trackPath). Note: this doesn't work for old Conservation tracks that don't have views/subtracks -- need to figure out how to fudge subtracks for those. 3. Limit the number of data sources to 5, don't let the user add the same data source twice, and remember which track the user most recently selected in the "Add Data Source" section. Also, in hgAi.jsx, I separated out a bunch of things that were rendered in the top-level AppComponent into subcomponents to add some granularity to "Loading..." sections (instead of making the whole page saying "Loading..." until everything is in place). diff --git src/hg/lib/cartJson.c src/hg/lib/cartJson.c index a0e4218..06cd76e 100644 --- src/hg/lib/cartJson.c +++ src/hg/lib/cartJson.c @@ -348,148 +348,186 @@ slFreeList(&valList); } else { // Regular single-value variable (or not in the cart): char *val = cartOptionalString(cj->cart, var->name); //#*** TODO: move jsonStringEscape inside jsonWriteString char *encoded = jsonStringEscape(val); jsonWriteString(cj->jw, var->name, encoded); freeMem(encoded); } } slFreeList(&varList); } -static struct slPair *trackLabelPairsFromTdbRefs(struct slRef *tdbRefList) -/* For each tdb in tdbRefList, make a pair of {track, shortLabel}. */ +INLINE boolean nameIsTdbField(char *name) +/* Return TRUE if name is a tdb->{field}, e.g. "track" or "shortLabel" etc. */ { -struct slPair *pairList = NULL; -struct slRef *slr; -for (slr = tdbRefList; slr != NULL; slr = slr->next) - { - struct trackDb *tdb = slr->val; - slAddHead(&pairList, slPairNew(tdb->track, tdb->shortLabel)); +static char *tdbFieldNames[] = + { "track", "table", "shortLabel", "longLabel", "type", "priority", "grp", "parent", + "subtracks", "visibility" }; +return (stringArrayIx(name, tdbFieldNames, ArraySize(tdbFieldNames)) >= 0); } -slReverse(&pairList); -return pairList; + +INLINE boolean fieldOk(char *field, struct hash *fieldHash) +/* Return TRUE if fieldHash is NULL or field exists in fieldHash. */ +{ +return (fieldHash == NULL || hashLookup(fieldHash, field)); } -static int trackDbRefCmpShortLabel(const void *va, const void *vb) -/* Case-insensitive comparison of max 32 chars of tdb->shortLabel in slRefs. */ +static void writeTdbSimple(struct jsonWrite *jw, struct trackDb *tdb, struct hash *fieldHash) +/* Write JSON for the non-parent/child fields of tdb */ +{ +if (fieldOk("track", fieldHash)) + jsonWriteString(jw, "track", tdb->track); +if (fieldOk("table", fieldHash)) + jsonWriteString(jw, "table", tdb->table); +if (fieldOk("shortLabel", fieldHash)) + jsonWriteString(jw, "shortLabel", tdb->shortLabel); +if (fieldOk("longLabel", fieldHash)) + jsonWriteString(jw, "longLabel", tdb->longLabel); +if (fieldOk("type", fieldHash)) + jsonWriteString(jw, "type", tdb->type); +if (fieldOk("priority", fieldHash)) + jsonWriteDouble(jw, "priority", tdb->priority); +if (fieldOk("grp", fieldHash)) + jsonWriteString(jw, "grp", tdb->grp); +// NOTE: if you add a new field here, then also add it to nameIsTdbField above. +if (tdb->settingsHash) + { + struct hashEl *hel; + struct hashCookie cookie = hashFirst(tdb->settingsHash); + while ((hel = hashNext(&cookie)) != NULL) + { + if (! nameIsTdbField(hel->name) && fieldOk(hel->name, fieldHash)) { -const struct slRef *aRef = *((struct slRef **)va); -const struct slRef *bRef = *((struct slRef **)vb); -const struct trackDb *tdbA = aRef->val, *tdbB = bRef->val; -char shortLabelA[33], shortLabelB[33]; -strncpy(shortLabelA, tdbA->shortLabel, 32); -shortLabelA[32] = '\0'; -strncpy(shortLabelB, tdbB->shortLabel, 32); -shortLabelB[32] = '\0'; -tolowers(shortLabelA); -tolowers(shortLabelB); -return strcmp(shortLabelA, shortLabelB); + //#*** TODO: move jsonStringEscape inside jsonWriteString + char *encoded = jsonStringEscape((char *)hel->val); + jsonWriteString(jw, hel->name, encoded); + } + } + } } -static void printAllTracks(struct cartJson *cj, struct trackDb *trackList) -/* Print a mapping of "allTracks" to {track, shortLabel} for all tracks, - * alphabetized by shortLabel. */ +static void rWriteTdb(struct jsonWrite *jw, struct trackDb *tdb, struct hash *fieldHash, + int depth, int maxDepth) +/* Recursively write JSON for tdb and its subtracks if any. If fieldHash is non-NULL, + * include only the field names indexed in fieldHash. */ +{ +if (maxDepth >= 0 && depth > maxDepth) + return; +jsonWriteObjectStart(jw, NULL); +writeTdbSimple(jw, tdb, fieldHash); +if (tdb->parent && fieldOk("parent", fieldHash)) + { + // We can't link to an object in JSON and better not recurse here or else infinite loop. + if (tdbIsSuperTrackChild(tdb)) + { + // Supertracks have been omitted from fullTrackList, so add the supertrack object's + // non-parent/child info here. + jsonWriteObjectStart(jw, "parent"); + writeTdbSimple(jw, tdb->parent, fieldHash); + jsonWriteObjectEnd(jw); + } + else + // Just the name so we don't have infinite loops. + jsonWriteString(jw, "parent", tdb->parent->track); + } +if (tdb->subtracks && fieldOk("subtracks", fieldHash)) { -struct slRef *refList = refListFromSlList(trackList); -slSort(&refList, trackDbRefCmpShortLabel); -struct slPair *trackLabelPairs = trackLabelPairsFromTdbRefs(refList); -jsonWriteValueLabelList(cj->jw, "allTracks", trackLabelPairs); + jsonWriteListStart(jw, "subtracks"); + struct trackDb *subTdb; + for (subTdb = tdb->subtracks; subTdb != NULL; subTdb = subTdb->next) + rWriteTdb(jw, subTdb, fieldHash, depth+1, maxDepth); + jsonWriteListEnd(jw); + } +jsonWriteObjectEnd(jw); } -static void printOneGroupTracks(struct cartJson *cj, char *groupName, struct slRef *tdbRefList) -/* Given a group and value is an slRef list of trackDbs, sort the list by trackDb priority - * and print out JSON mapping the group name to list of trackDb {value, label} objects. */ +static struct hash *hashFromCommaString(char *commaString) +/* Return a hash that stores words in comma-separated string, or NULL if string is NULL or empty. */ +{ +struct hash *hash = NULL; +if (isNotEmpty(commaString)) { -slSort(&tdbRefList, trackDbRefCmp); -struct slPair *trackLabelPairs = trackLabelPairsFromTdbRefs(tdbRefList); -jsonWriteValueLabelList(cj->jw, groupName, trackLabelPairs); + hash = hashNew(0); + char *words[1024]; + int i, wordCount = chopCommas(commaString, words); + for (i = 0; i < wordCount; i++) + hashStoreName(hash, words[i]); + } +return hash; } -static void printGroupTracks(struct cartJson *cj, struct trackDb *trackList) -/* Hash group names to lists of tracks in those groups, and print out the structure as JSON. */ +static struct hash *hashTracksByGroup(struct trackDb *trackList) +/* Hash group names to lists of tracks in those groups; sort each list by trackDb priority. */ { struct hash *hash = hashNew(0); struct trackDb *tdb; for (tdb = trackList; tdb != NULL; tdb = tdb->next) { struct hashEl *hel = hashLookup(hash, tdb->grp); struct slRef *slr = slRefNew(tdb); if (hel) slAddHead(&(hel->val), slr); else hashAdd(hash, tdb->grp, slr); } -jsonWriteObjectStart(cj->jw, "groupTracks"); -printAllTracks(cj, trackList); struct hashCookie cookie = hashFirst(hash); struct hashEl *hel; while ((hel = hashNext(&cookie)) != NULL) - { - char *groupName = hel->name; - struct slRef *tdbRefList = hel->val; - printOneGroupTracks(cj, groupName, tdbRefList); - } -jsonWriteObjectEnd(cj->jw); -hashFree(&hash); -} - -static void printTrackTables(struct cartJson *cj, struct trackDb *trackList) -/* Hash track names to lists of tables in those tracks, and print out the structure as JSON. */ -{ -jsonWriteObjectStart(cj->jw, "trackTables"); -char *db = cartString(cj->cart, "db"); -struct trackDb *tdb; -for (tdb = trackList; tdb != NULL; tdb = tdb->next) - { - struct slName *tableList = cartTrackDbTablesForTrack(db, tdb, FALSE); // no useJoiner for now - jsonWriteSlNameList(cj->jw, tdb->track, tableList); - slFreeList(&tableList); - } -jsonWriteObjectEnd(cj->jw); + slSort(&hel->val, trackDbRefCmp); +return hash; } -void cartJsonGetGroupsTracksTables(struct cartJson *cj, struct hash *paramHash) -/* Print info necessary for group/track/table menus. */ +void cartJsonGetGroupedTrackDb(struct cartJson *cj, struct hash *paramHash) +/* Translate trackDb list (only a subset of the fields) into JSON array of track group objects; + * each group contains an array of track objects that may have subtracks. */ { struct trackDb *fullTrackList = NULL; struct grp *fullGroupList = NULL; cartTrackDbInit(cj->cart, &fullTrackList, &fullGroupList, /* useAccessControl=*/TRUE); -// Print out options for the track group menu: -// Remove All Tracks & All Tables from the end of the list for now. -struct grp *grp, *nextGrp = NULL; -for (grp = fullGroupList; grp != NULL; grp = nextGrp) +struct hash *groupedTrackRefList = hashTracksByGroup(fullTrackList); +// If the optional param 'fields' is given, hash the field names that should be returned. +char *fields = cartJsonOptionalParam(paramHash, "fields"); +struct hash *fieldHash = hashFromCommaString(fields); +// Also check for optional parameter 'maxDepth': +int maxDepth = -1; +char *maxDepthStr = cartJsonOptionalParam(paramHash, "maxDepth"); +if (isNotEmpty(maxDepthStr)) + maxDepth = atoi(maxDepthStr); +struct jsonWrite *jw = cj->jw; +jsonWriteListStart(jw, "groupedTrackDb"); +struct grp *grp; +for (grp = fullGroupList; grp != NULL; grp = grp->next) { - nextGrp = grp->next; - if (nextGrp && (sameString(nextGrp->name, "allTracks") || - sameString(nextGrp->name, "allTables"))) + jsonWriteObjectStart(jw, NULL); + jsonWriteString(jw, "name", grp->name); + jsonWriteString(jw, "label", grp->label); + jsonWriteListStart(jw, "tracks"); + struct slRef *tdbRefList = hashFindVal(groupedTrackRefList, grp->name); + struct slRef *tdbRef; + for (tdbRef = tdbRefList; tdbRef != NULL; tdbRef = tdbRef->next) { - grp->next = nextGrp->next; - nextGrp = grp; + struct trackDb *tdb = tdbRef->val; + rWriteTdb(jw, tdb, fieldHash, 1, maxDepth); } + jsonWriteListEnd(jw); + jsonWriteObjectEnd(jw); } -jsonWriteObjectStart(cj->jw, "trackDbInfo"); -jsonWriteValueLabelList(cj->jw, "groupOptions", (struct slPair *)fullGroupList); -// Print out an object that maps group names to their tracks: -printGroupTracks(cj, fullTrackList); -// Print out an object that maps track names to tables: -printTrackTables(cj, fullTrackList); -jsonWriteObjectEnd(cj->jw); +jsonWriteListEnd(jw); } static char *hAssemblyDescription(char *db) /* Return a string containing db's description.html, or NULL if not found. */ //#*** LIBIFY: Code lifted from hgFind.c's hgPositionsHelpHtml. { char *htmlPath = hHtmlPath(db); char *htmlString = NULL; if (htmlPath != NULL) { if (fileExists(htmlPath)) readInGulp(htmlPath, &htmlString, NULL); else if (startsWith("http://" , htmlPath) || startsWith("https://", htmlPath) || startsWith("ftp://" , htmlPath)) @@ -550,77 +588,83 @@ while ((hel = hashNext(&cookie)) != NULL) { if (isEmpty(cartOptionalString(cj->cart, hel->name))) { char *val = jsonStringVal((struct jsonElement *)(hel->val), hel->name); if (val) cartSetString(cj->cart, hel->name, val); } } } static void getCladeOrgDbPos(struct cartJson *cj, struct hash *paramHash) /* Get cart's current clade, org, db, position and geneSuggest track. */ { char *db = cartString(cj->cart, "db"); -char *genome = hGenome(db); -char *clade = hClade(genome); +char *genome = cartUsualString(cj->cart, "org", hGenome(db)); +char *clade = cartUsualString(cj->cart, "clade", hClade(genome)); printClade(cj->jw, clade); printOrg(cj->jw, clade, genome); printDb(cj->jw, genome, db); char *position = cartOptionalString(cj->cart, "position"); if (isEmpty(position)) position = hDefaultPos(db); printPosition(cj->jw, position); -printGeneSuggestTrack(cj, cartString(cj->cart, "db")); +printGeneSuggestTrack(cj, db); } static void getStaticHtml(struct cartJson *cj, struct hash *paramHash) /* Read HTML text from a relative path under browser.documentRoot and * write it as an encoded JSON string */ { char *tag = cartJsonOptionalParam(paramHash, "tag"); if (isEmpty(tag)) tag = "html"; char *file = cartJsonRequiredParam(paramHash, "file", cj->jw, "getStaticHtml"); char *html = hFileContentsOrWarning(file); //#*** TODO: move jsonStringEscape inside jsonWriteString char *encoded = jsonStringEscape(html); jsonWriteString(cj->jw, tag, encoded); } void cartJsonRegisterHandler(struct cartJson *cj, char *command, CartJsonHandler *handler) /* Associate handler with command; when javascript sends command, handler will be executed. */ { hashAdd(cj->handlerHash, command, handler); } struct cartJson *cartJsonNew(struct cart *cart) /* Allocate and return a cartJson object with default handlers. */ { struct cartJson *cj; AllocVar(cj); cj->handlerHash = hashNew(0); cj->jw = jsonWriteNew(); cj->cart = cart; +char *db = cartOptionalString(cj->cart, "db"); +if (isEmpty(db) || ! hDbIsActive(db)) + { + db = hDefaultDb(); + cartSetString(cj->cart, "db", db); + } cartJsonRegisterHandler(cj, "getCladeOrgDbPos", getCladeOrgDbPos); cartJsonRegisterHandler(cj, "changeClade", cartJsonChangeClade); cartJsonRegisterHandler(cj, "changeOrg", cartJsonChangeOrg); cartJsonRegisterHandler(cj, "changeDb", cartJsonChangeDb); cartJsonRegisterHandler(cj, "changePosition", changePositionHandler); cartJsonRegisterHandler(cj, "get", getVar); -cartJsonRegisterHandler(cj, "getGroupsTracksTables", cartJsonGetGroupsTracksTables); +cartJsonRegisterHandler(cj, "getGroupedTrackDb", cartJsonGetGroupedTrackDb); cartJsonRegisterHandler(cj, "getAssemblyInfo", getAssemblyInfo); cartJsonRegisterHandler(cj, "getHasCustomTracks", getHasCustomTracks); cartJsonRegisterHandler(cj, "getIsSpecialHost", getIsSpecialHost); cartJsonRegisterHandler(cj, "getHasHubTable", getHasHubTable); cartJsonRegisterHandler(cj, "setIfUnset", setIfUnset); cartJsonRegisterHandler(cj, "getStaticHtml", getStaticHtml); return cj; } void cartJsonFree(struct cartJson **pCj) /* Close **pCj's contents and nullify *pCj. */ { if (*pCj == NULL) return; struct cartJson *cj = *pCj;